Build a VS Code Extension in TypeScript
Build a VS Code extension that visually highlights dead code in real-time.
Introduction
30 Second Summary
Every codebase accumulates unused variables, abandoned CSS classes, and leftover style properties that clutter your files and slow down your team. These "dead code" remnants hide in plain sight, making files harder to read and maintain.
In this project, you will build a VS Code extension in TypeScript that automatically detects dead code and fades it out directly in your editor. No API keys, no cloud services. Just a local developer tool you can package and share.
What You'll Build
A professional-grade VS Code extension called Dead Code Hunter that scans JavaScript, TypeScript, CSS, and HTML files for unused style properties, CSS classes, and variables, then visually highlights them with reduced opacity and red wavy underlines.
By the end of this project, you'll have:
- A complete VS Code extension with real-time code scanning that highlights dead code as you type.
- A fully configured extension manifest with commands, user-facing settings, activation events, and a proper TypeScript build pipeline.
- A Webview report panel that displays all detected dead code issues in a structured table.
- Secret Mission: Publish your extension to the VS Code Marketplace so anyone in the world can install it.
Are there any prerequisites?
You should have basic familiarity with TypeScript or JavaScript (variables, functions, objects). You should also be comfortable using VS Code as your editor.
Before We Start
VS Code extensions are built with TypeScript and run directly inside the editor. To build our Dead Code Hunter extension, we first need a properly configured TypeScript project that VS Code can compile and load.
In this step, you will install Node.js, create your project folder, pull in the required TypeScript packages, and configure the compiler.
In this step, get ready to:
- Install Node.js and verify your version.
- Create the project folder and install TypeScript dependencies.
- Configure the TypeScript compiler.
Verify your Node.js installation
VS Code extensions require Node.js for package management and TypeScript compilation. This project needs version 20 or higher.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Check your Node.js version by running this command:
node --version
✔️ I see version 20 or higher
Your Node.js version is compatible. Confirm npm is also available by running:
npm --version
You should see a version number printed. You are all set to continue.
ⓧ I see an older version
You need Node.js v20 or higher for this project. Your current version is too old.
- Go to https://nodejs.org/en/download and download the LTS installer for your operating system.
- Run the installer and accept the defaults.
- Close and reopen your terminal, then run node --version again to confirm it shows v20 or higher.
Why do I need to reopen my terminal?
Your terminal caches the path to programs when it starts. Reopening it forces the terminal to pick up the new Node.js installation.
ⓧ Command not found
Node.js is not installed on your machine yet.
- Go to https://nodejs.org/en/download and download the LTS installer for your operating system.
- Run the installer and accept the defaults.
- Close and reopen your terminal, then run node --version again to confirm it shows v20 or higher.
- Also confirm npm is available by running npm --version.
What is Node.js?
Node.js is a JavaScript runtime that runs outside the browser. VS Code extensions use it for package management (npm) and for compiling TypeScript into JavaScript.
Create the project folder and install dependencies
Every VS Code extension lives in its own folder with a package.json manifest. You will create this folder on your Desktop so it is easy to find.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir dead-code-hunter
cd dead-code-hunter
- Confirm the folder was created by running:
ls
You should see an empty output since the folder is brand new.
Seeing 'mkdir: cannot create directory'?
This means a folder called dead-code-hunter already exists on your Desktop. Delete the existing folder first, or run cd dead-code-hunter to move into the one that already exists.
help me fix this mkdir error
- Initialize a new Node.js project by running:
npm init -y
You should see a confirmation that package.json was created.
What does npm init -y do?
This command creates a package.json file with sensible defaults. The -y flag skips all the interactive prompts and accepts the defaults automatically.
The package.json file is the manifest for any Node.js project. It tracks your project name, version, scripts, and dependencies.
- Install the TypeScript development dependencies by running:
npm install --save-dev typescript @types/vscode @types/node
You should see output confirming packages were added. A node_modules folder now exists in your project.
What are these packages?
- typescript is the TypeScript compiler that converts .ts files into JavaScript that VS Code can run.
- @types/vscode provides TypeScript type definitions for the VS Code Extension API. This gives you autocomplete and type checking for VS Code functions.
- @types/node provides type definitions for built-in Node.js modules like path and fs.
npm install taking too long or failing?
Check that you have an internet connection. If you see permission errors on macOS, try running the command with sudo in front. On Windows, try running your terminal as Administrator.
help me fix this npm install error
- Open VS Code by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Select the dead-code-hunter folder on your Desktop, then click Open.
You should see dead-code-hunter in the VS Code title bar and a package.json file in the Explorer sidebar.
Create the TypeScript compiler configuration
TypeScript needs a configuration file to know how to compile your code. This file tells the compiler where to find source files, where to output compiled JavaScript, and which strictness rules to enforce.
- In VS Code's Explorer sidebar, hover over the DEAD-CODE-HUNTER heading to reveal action icons.
- Click the New File icon (looks like a page with a plus sign).
- Type tsconfig.json as the file name and press Enter.
- Paste the following configuration into the file:
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2022",
"lib": ["ES2022"],
"outDir": "out",
"rootDir": "src",
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "out"]
}
What does this configure?
- module set to commonjs means the output uses Node.js-style require() imports. VS Code extensions must use this format.
- outDir set to out means compiled JavaScript lands in an out/ folder, keeping source and output separate.
- rootDir set to src tells the compiler to look for TypeScript files inside a src/ folder.
- strict set to true enables all strict type-checking options for maximum type safety.
- sourceMap set to true generates .map files so you can debug TypeScript directly instead of the compiled JavaScript.
- Save tsconfig.json by pressing Cmd+S (macOS) or Ctrl+S (Windows).
You should see tsconfig.json appear in the Explorer sidebar alongside package.json and the node_modules folder.
File not appearing in the sidebar?
Make sure you created the file at the root of the dead-code-hunter folder, not inside node_modules. If you accidentally nested it, drag tsconfig.json to the top level in the Explorer sidebar.
help me find my tsconfig.json file
✔️ Awesome, I've got everything!
Great. Double check you have saved tsconfig.json and you are ready to move on.
ⓧ I'd like to double check the full code
Here is the complete tsconfig.json file. Make sure yours matches exactly:
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2022",
"lib": ["ES2022"],
"outDir": "out",
"rootDir": "src",
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "out"]
}
Your TypeScript foundation is ready. Next up, you will build the full extension manifest and configure VS Code to launch and debug your extension.
Create the Extension Manifest and Project Structure
Your project folder and TypeScript compiler are ready. Now it's time to tell VS Code what your extension actually does.
Every VS Code extension is driven by its manifest file. The package.json manifest declares what commands your extension offers, what settings users can configure, and which file types trigger it to activate. You will also set up debugging so you can test your extension with a single keypress.
In this step, get ready to:
- Write the extension manifest with commands, settings, and activation events.
- Create debugging and build configuration files.
- Exclude source files from the packaged extension.
Write the extension manifest
The default package.json from npm init is a bare-bones Node.js config. A VS Code extension manifest needs far more: activation events, contributed commands, user-facing settings, and build scripts.
- Replace the entire contents of package.json with the following:
{
"name": "dead-code-hunter",
"displayName": "Dead Code Hunter",
"description": "Fades out unused style properties, CSS classes, and variables \u2014 entirely offline, no API keys needed.",
"version": "0.1.0",
"publisher": "your-publisher-name",
"engines": {
"vscode": "^1.74.0"
},
"categories": ["Linters", "Other"],
"icon": "icon.png",
"activationEvents": [
"onLanguage:javascript",
"onLanguage:javascriptreact",
"onLanguage:typescript",
"onLanguage:typescriptreact",
"onLanguage:css",
"onLanguage:scss",
"onLanguage:less",
"onLanguage:html",
"onLanguage:vue"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "deadCodeHunter.hunt",
"title": "Hunt Dead Code",
"category": "Dead Code Hunter"
},
{
"command": "deadCodeHunter.showReport",
"title": "Show Dead Code Report",
"category": "Dead Code Hunter"
},
{
"command": "deadCodeHunter.clear",
"title": "Clear Dead Code Highlights",
"category": "Dead Code Hunter"
}
],
"configuration": {
"title": "Dead Code Hunter",
"properties": {
"deadCodeHunter.enableOnSave": {
"type": "boolean",
"default": true,
"description": "Auto-scan every time you save."
},
"deadCodeHunter.enableRealtime": {
"type": "boolean",
"default": true,
"description": "Scan in real-time as you type."
},
"deadCodeHunter.debounceDelay": {
"type": "number",
"default": 500,
"description": "Milliseconds to wait after typing before scanning."
},
"deadCodeHunter.highlightColor": {
"type": "string",
"default": "rgba(255, 80, 80, 0.10)",
"description": "Background highlight color for dead code."
},
"deadCodeHunter.opacity": {
"type": "string",
"default": "0.35",
"description": "Opacity of dead code text (0 = invisible, 1 = fully opaque)."
},
"deadCodeHunter.scanStyleObjects": {
"type": "boolean",
"default": true,
"description": "Scan for unused style object keys."
},
"deadCodeHunter.scanCssClasses": {
"type": "boolean",
"default": true,
"description": "Scan for unused CSS class declarations."
},
"deadCodeHunter.scanVariables": {
"type": "boolean",
"default": true,
"description": "Scan for unused variable declarations."
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./"
},
"devDependencies": {
"@types/vscode": "^1.74.0",
"@types/node": "^20.0.0",
"typescript": "^5.7.0"
}
}
What does this manifest configure?
This file configures several critical aspects of your extension:
- activationEvents tells VS Code to load your extension when a JavaScript, TypeScript, CSS, HTML, or Vue file is opened.
- contributes.commands registers three commands users can run from the Command Palette.
- contributes.configuration exposes user-facing settings like opacity, highlight color, and which scanners are active.
- main points to ./out/extension.js because VS Code runs the compiled JavaScript, not your TypeScript source.
What are activation events?
Activation events control when VS Code loads your extension into memory. Using onLanguage events means your extension only activates when the user opens a relevant file type. This keeps VS Code fast by avoiding unnecessary startup work.
- Save package.json.
Seeing red squiggles in package.json?
Make sure you replaced the entire file contents, not just appended to the existing JSON. The file must contain exactly one top-level object.
Check for trailing commas after the last item in any array or object. JSON does not allow trailing commas.
help me fix JSON syntax errors in my package.json
✔️ Awesome, I've got everything!
Great. Make sure you have saved your file before moving on.
ⓧ I'd like to double check the full code
{
"name": "dead-code-hunter",
"displayName": "Dead Code Hunter",
"description": "Fades out unused style properties, CSS classes, and variables \u2014 entirely offline, no API keys needed.",
"version": "0.1.0",
"publisher": "your-publisher-name",
"engines": {
"vscode": "^1.74.0"
},
"categories": ["Linters", "Other"],
"icon": "icon.png",
"activationEvents": [
"onLanguage:javascript",
"onLanguage:javascriptreact",
"onLanguage:typescript",
"onLanguage:typescriptreact",
"onLanguage:css",
"onLanguage:scss",
"onLanguage:less",
"onLanguage:html",
"onLanguage:vue"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "deadCodeHunter.hunt",
"title": "Hunt Dead Code",
"category": "Dead Code Hunter"
},
{
"command": "deadCodeHunter.showReport",
"title": "Show Dead Code Report",
"category": "Dead Code Hunter"
},
{
"command": "deadCodeHunter.clear",
"title": "Clear Dead Code Highlights",
"category": "Dead Code Hunter"
}
],
"configuration": {
"title": "Dead Code Hunter",
"properties": {
"deadCodeHunter.enableOnSave": {
"type": "boolean",
"default": true,
"description": "Auto-scan every time you save."
},
"deadCodeHunter.enableRealtime": {
"type": "boolean",
"default": true,
"description": "Scan in real-time as you type."
},
"deadCodeHunter.debounceDelay": {
"type": "number",
"default": 500,
"description": "Milliseconds to wait after typing before scanning."
},
"deadCodeHunter.highlightColor": {
"type": "string",
"default": "rgba(255, 80, 80, 0.10)",
"description": "Background highlight color for dead code."
},
"deadCodeHunter.opacity": {
"type": "string",
"default": "0.35",
"description": "Opacity of dead code text (0 = invisible, 1 = fully opaque)."
},
"deadCodeHunter.scanStyleObjects": {
"type": "boolean",
"default": true,
"description": "Scan for unused style object keys."
},
"deadCodeHunter.scanCssClasses": {
"type": "boolean",
"default": true,
"description": "Scan for unused CSS class declarations."
},
"deadCodeHunter.scanVariables": {
"type": "boolean",
"default": true,
"description": "Scan for unused variable declarations."
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./"
},
"devDependencies": {
"@types/vscode": "^1.74.0",
"@types/node": "^20.0.0",
"typescript": "^5.7.0"
}
}
Set up F5 debugging
VS Code extensions have a special debugging workflow. Pressing F5 launches a second VS Code window called the Extension Development Host, which loads your extension from source. You need two config files to make this work.
- Create the .vscode folder inside your dead-code-hunter project by running this command in the terminal:
mkdir .vscode
- Create a new file called launch.json inside the .vscode folder and paste the following:
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "${defaultBuildTask}"
}
]
}
What does this launch configuration do?
- extensionHost tells VS Code to launch a new window with your extension loaded.
- --extensionDevelopmentPath points to your project folder so VS Code knows where to find the extension.
- preLaunchTask compiles your TypeScript before launching, so you always test the latest code.
- Save .vscode/launch.json.
- Create another file called tasks.json inside the same .vscode folder and paste the following:
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
What does the tasks file do?
This defines a background task that runs tsc -watch via the watch npm script. It recompiles your TypeScript every time you save a file. Because it is set as the default build task, launch.json automatically runs it before launching the Extension Development Host.
- Save .vscode/tasks.json.
- Confirm both files exist by running:
ls .vscode
You should see launch.json and tasks.json in the output.
Don't see both files?
Make sure you created both files inside the .vscode folder, not in the project root. The folder name starts with a dot, which makes it hidden on macOS and Linux.
help me find my .vscode folder and create the config files
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before continuing.
ⓧ I'd like to double check the full code
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "${defaultBuildTask}"
}
]
}
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
Create .vscodeignore
When you package your extension for distribution, you only want to ship the compiled output. Source files, config files, and test files add unnecessary weight to the package.
- Create a new file called .vscodeignore in the root of your dead-code-hunter folder (not inside .vscode) and paste the following:
.vscode/**
src/**
node_modules/**
.gitignore
tsconfig.json
sample-test.js
What does .vscodeignore do?
This file works like .gitignore but for extension packaging. Any file or folder listed here will be excluded when you run vsce package later. Only the compiled out/ directory and package.json will ship in the final .vsix file.
- Save .vscodeignore.
- In the VS Code Explorer sidebar, confirm you can see .vscodeignore at the project root alongside package.json and tsconfig.json.
Don't see .vscodeignore in Explorer?
Files starting with a dot are sometimes hidden. In the Explorer sidebar, check that no filter is hiding dotfiles. You can also verify from the terminal by running ls -a to list all files including hidden ones.
help me find my .vscodeignore file
✔️ Awesome, I've got everything!
Great. Double check you have saved all files before continuing.
ⓧ I'd like to double check the full code
.vscode/**
src/**
node_modules/**
.gitignore
tsconfig.json
sample-test.js
Your extension's skeleton is complete. VS Code now knows what commands to register, what settings to expose, and how to compile and debug your TypeScript. Next up, you will write the actual scanning engine that finds dead code.
Build the Extension Engine
Your project structure is ready. Now it is time to write the actual scanning engine that powers your extension.
The VS Code Extension API gives you hooks into the editor lifecycle. Your extension will use these hooks to scan code in real-time, highlight dead code with decorations, and display results in the status bar.
In this step, get ready to:
- Create the extension source file with a typed activation function and decoration styles.
- Implement three regex-based scanners for style objects, CSS classes, and variables.
- Wire up the scan dispatcher, status bar display, and a webview report panel.
Create the extension file with the activate function
Every VS Code extension needs an activate function. This is the entry point VS Code calls when your extension loads.
- Create a src folder inside your dead-code-hunter project by running this command:
mkdir src
- Create a new file called extension.ts inside the src folder.
- In VS Code, click **File** in the top menu bar.
- Click New File.
- Save it as src/extension.ts.
- Add the file header with the import, interface, and module-level variables by pasting this code:
import * as vscode from 'vscode';
// Define the shape of a dead code finding
interface DeadCodeIssue {
range: vscode.Range;
message: string;
}
// Module-level state shared across all functions
let deadDecorationType: vscode.TextEditorDecorationType;
let statusBarItem: vscode.StatusBarItem;
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
let currentIssues: DeadCodeIssue[] = [];
What does this code do?
The DeadCodeIssue interface defines the shape of each finding: a text range in the document and a human-readable message.
The four module-level variables hold shared state. deadDecorationType controls how dead code looks visually. statusBarItem shows the issue count. debounceTimer prevents scanning on every single keystroke.
You won't see a change yet. The next chunk builds the activate function that uses these variables.
- Below the module-level variables, add the complete activate function:
export function activate(context: vscode.ExtensionContext): void {
// Initialize the decoration style on startup
updateDecorationStyle();
// Create a status bar item that shows dead code count
statusBarItem = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Right,
100
);
statusBarItem.command = 'deadCodeHunter.showReport';
context.subscriptions.push(statusBarItem);
// Register the "Hunt Dead Code" command
const huntCommand = vscode.commands.registerCommand(
'deadCodeHunter.hunt',
() => {
const editor = vscode.window.activeTextEditor;
if (editor) {
scanDocument(editor);
}
}
);
// Register the "Clear Highlights" command
const clearCommand = vscode.commands.registerCommand(
'deadCodeHunter.clear',
() => {
const editor = vscode.window.activeTextEditor;
if (editor) {
editor.setDecorations(deadDecorationType, []);
currentIssues = [];
updateStatusBar(0);
}
}
);
// Register the "Show Report" command
const reportCommand = vscode.commands.registerCommand(
'deadCodeHunter.showReport',
() => {
showReport();
}
);
context.subscriptions.push(huntCommand, clearCommand, reportCommand);
// Re-scan when the user switches to a different file
vscode.window.onDidChangeActiveTextEditor(
(editor) => {
if (editor) {
scanDocument(editor);
}
},
null,
context.subscriptions
);
// Real-time scanning with debounce as the user types
vscode.workspace.onDidChangeTextDocument(
(event) => {
const config = vscode.workspace.getConfiguration('deadCodeHunter');
if (!config.get<boolean>('enableRealtime')) {
return;
}
const editor = vscode.window.activeTextEditor;
if (editor && event.document === editor.document) {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = setTimeout(() => {
scanDocument(editor);
}, config.get<number>('debounceDelay') ?? 500);
}
},
null,
context.subscriptions
);
// Scan on file save
vscode.workspace.onDidSaveTextDocument(
(document) => {
const config = vscode.workspace.getConfiguration('deadCodeHunter');
if (!config.get<boolean>('enableOnSave')) {
return;
}
const editor = vscode.window.activeTextEditor;
if (editor && editor.document === document) {
scanDocument(editor);
}
},
null,
context.subscriptions
);
// Re-apply styles when user changes extension settings
vscode.workspace.onDidChangeConfiguration(
(event) => {
if (event.affectsConfiguration('deadCodeHunter')) {
updateDecorationStyle();
const editor = vscode.window.activeTextEditor;
if (editor) {
scanDocument(editor);
}
}
},
null,
context.subscriptions
);
// Run an initial scan if a file is already open
if (vscode.window.activeTextEditor) {
scanDocument(vscode.window.activeTextEditor);
}
}
What does this function do?
The activate function is the entry point VS Code calls when your extension loads. It does four things:
- Creates a status bar item that displays the dead code count and opens the report when clicked.
- Registers three commands (hunt, clear, showReport) that the user can trigger from the Command Palette.
- Attaches event listeners for real-time scanning. The onDidChangeTextDocument listener uses a debounce timer so it waits 500ms after typing stops before scanning.
- Pushes all disposables into context.subscriptions so VS Code cleans them up automatically.
Why debounce instead of scanning on every keystroke?
If the extension scanned on every single character typed, it would fire hundreds of regex passes per minute. The debounce waits until the user pauses typing, then scans once. This keeps the editor responsive.
You won't see a change yet. The functions activate calls (like scanDocument and updateDecorationStyle) are defined in the next chunks.
- Below the activate function, add the updateDecorationStyle function:
function updateDecorationStyle(): void {
// Dispose old style before creating a new one
if (deadDecorationType) {
deadDecorationType.dispose();
}
// Read user-configurable settings
const config = vscode.workspace.getConfiguration('deadCodeHunter');
const highlightColor = config.get<string>('highlightColor') ?? 'rgba(255, 80, 80, 0.10)';
const opacity = config.get<string>('opacity') ?? '0.35';
// Create the visual decoration: faded text + wavy underline + scrollbar marker
deadDecorationType = vscode.window.createTextEditorDecorationType({
opacity: opacity,
backgroundColor: highlightColor,
textDecoration: 'underline wavy rgba(255, 80, 80, 0.7)',
overviewRulerColor: 'rgba(255, 80, 80, 0.8)',
overviewRulerLane: vscode.OverviewRulerLane.Right,
});
}
What does this function do?
This function creates (or recreates) the decoration type that controls how dead code looks in the editor. It reads the user's configured opacity and highlight color, then builds a decoration with three visual indicators:
- Reduced opacity fades the dead code text.
- A wavy red textDecoration underlines it.
- An overviewRulerColor places a red marker in the scrollbar so you can spot issues from a distance.
You won't see a change yet. The next substep adds the scanner functions that produce findings to decorate.
Implement the three scanners
The extension uses three regex-based scanners. Each scanner searches the document text for declarations, then checks whether those declarations are actually used elsewhere in the file.
- Below updateDecorationStyle, add the scanDocument dispatcher function that routes to the correct scanner based on language:
function scanDocument(editor: vscode.TextEditor): void {
const document = editor.document;
const languageId = document.languageId;
const text = document.getText();
const config = vscode.workspace.getConfiguration('deadCodeHunter');
let issues: DeadCodeIssue[] = [];
// Language groups determine which scanners to run
const jsLangs = ['javascript', 'javascriptreact', 'typescript', 'typescriptreact'];
const cssLangs = ['css', 'scss', 'less'];
const htmlLangs = ['html', 'vue'];
if (jsLangs.includes(languageId)) {
if (config.get<boolean>('scanStyleObjects')) {
issues = issues.concat(scanStyleObjects(text, document));
}
if (config.get<boolean>('scanVariables')) {
issues = issues.concat(scanVariables(text, document));
}
}
if (cssLangs.includes(languageId)) {
if (config.get<boolean>('scanCssClasses')) {
issues = issues.concat(scanCssClasses(text, document));
}
}
if (htmlLangs.includes(languageId)) {
if (config.get<boolean>('scanCssClasses')) {
issues = issues.concat(scanCssClasses(text, document));
}
if (config.get<boolean>('scanVariables')) {
issues = issues.concat(scanVariables(text, document));
}
}
// Store issues for the report panel
currentIssues = issues;
// Convert issues to VS Code decorations with hover messages
const decorations: vscode.DecorationOptions[] = issues.map((issue) => ({
range: issue.range,
hoverMessage: new vscode.MarkdownString(`🪲 **Dead Code:** ${issue.message}`),
}));
// Apply decorations to the editor and update the status bar
editor.setDecorations(deadDecorationType, decorations);
updateStatusBar(issues.length);
}
What does this function do?
This is the central dispatcher. It checks the file's language (JavaScript, CSS, HTML, etc.) and calls the appropriate scanners. Each scanner returns an array of DeadCodeIssue objects. The dispatcher then converts those issues into VS Code decorations and applies them to the editor.
The hoverMessage property means learners will see a tooltip explaining the issue when they hover over highlighted code.
You won't see a change yet. The scanner functions referenced above are added next.
- Below scanDocument, add the style object scanner:
function scanStyleObjects(text: string, document: vscode.TextDocument): DeadCodeIssue[] {
const issues: DeadCodeIssue[] = [];
// Match style blocks like: const styles = { key: value }
const styleBlockRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:StyleSheet\.create\()?{([^}]*)}/gs;
let blockMatch: RegExpExecArray | null;
while ((blockMatch = styleBlockRegex.exec(text)) !== null) {
const varName = blockMatch[1];
const blockContent = blockMatch[2];
const blockStart = blockMatch.index + blockMatch[0].indexOf(blockContent);
// Find each key inside the style block
const keyRegex = /(\w+)\s*:/g;
let keyMatch: RegExpExecArray | null;
while ((keyMatch = keyRegex.exec(blockContent)) !== null) {
const keyName = keyMatch[1];
// Check if the key is referenced outside the block (e.g. styles.container)
const usagePattern = new RegExp(
`${varName}\.${keyName}|${varName}\\[['"\`]${keyName}['"\`]\\]`,
'g'
);
const textBeforeBlock = text.substring(0, blockMatch.index);
const textAfterBlock = text.substring(blockMatch.index + blockMatch[0].length);
const outsideText = textBeforeBlock + textAfterBlock;
if (!usagePattern.test(outsideText)) {
const absolutePos = blockStart + keyMatch.index;
const startPos = document.positionAt(absolutePos);
const endPos = document.positionAt(absolutePos + keyName.length);
issues.push({
range: new vscode.Range(startPos, endPos),
message: `Style key "${keyName}" is never referenced as ${varName}.${keyName}`,
});
}
}
}
return issues;
}
What does this scanner do?
This scanner finds style objects (like const styles = { container: {...} }) and checks whether each key is actually used elsewhere in the file. If styles.container never appears outside the declaration block, the key is flagged as dead.
It also catches React Native's StyleSheet.create() pattern.
You won't see a change yet. The CSS class scanner is next.
- Below scanStyleObjects, add the CSS class scanner:
function scanCssClasses(text: string, document: vscode.TextDocument): DeadCodeIssue[] {
const issues: DeadCodeIssue[] = [];
// Match CSS class declarations like .className {
const classRegex = /\.([a-zA-Z_][\w-]*)\s*{/g;
let match: RegExpExecArray | null;
while ((match = classRegex.exec(text)) !== null) {
const className = match[1];
// Check if the class name appears in a string or class attribute
const usagePattern = new RegExp(
`["'\`\\s]${className}["'\`\\s]|class=["'][^"']*\\b${className}\\b[^"']*["']`,
'g'
);
// Blank out the declaration itself so it doesn't match its own name
const textWithoutDeclaration =
text.substring(0, match.index) +
' '.repeat(match[0].length) +
text.substring(match.index + match[0].length);
if (!usagePattern.test(textWithoutDeclaration)) {
const dotPos = match.index;
const startPos = document.positionAt(dotPos);
const endPos = document.positionAt(dotPos + match[0].length - 1);
issues.push({
range: new vscode.Range(startPos, endPos),
message: `CSS class ".${className}" is never referenced in this file`,
});
}
}
return issues;
}
What does this scanner do?
This scanner finds CSS class declarations (like .card {) and checks whether that class name appears anywhere else in the file. It blanks out the declaration line itself so the class name does not falsely match against its own definition.
You won't see a change yet. One more scanner to go.
- Below scanCssClasses, add the variable scanner:
function scanVariables(text: string, document: vscode.TextDocument): DeadCodeIssue[] {
const issues: DeadCodeIssue[] = [];
// Match variable declarations: const x =, let x =, var x =
const varRegex = /(?:const|let|var)\s+([a-zA-Z_$][\w$]*)\s*=/g;
let match: RegExpExecArray | null;
while ((match = varRegex.exec(text)) !== null) {
const varName = match[1];
// Skip common special names that are used implicitly
if (varName === 'styles' || varName === 'module' || varName === 'exports') {
continue;
}
// Check if the variable name appears anywhere AFTER its declaration
const afterDeclaration = text.substring(match.index + match[0].length);
const usagePattern = new RegExp(`\\b${varName}\\b`);
if (!usagePattern.test(afterDeclaration)) {
const nameStart = match.index + match[0].indexOf(varName);
const startPos = document.positionAt(nameStart);
const endPos = document.positionAt(nameStart + varName.length);
issues.push({
range: new vscode.Range(startPos, endPos),
message: `Variable "${varName}" is declared but never read`,
});
}
}
return issues;
}
What does this scanner do?
This scanner finds const, let, and var declarations and checks if the variable name ever appears again after its declaration. If not, it is dead code.
It skips styles, module, and exports since those are commonly used implicitly by frameworks or bundlers.
- Save src/extension.ts.
You won't see a change yet. The final substep adds the remaining utility functions so the file compiles.
Seeing red squiggles in the file?
TypeScript may show errors because the file references functions that do not exist yet (like updateStatusBar and showReport). These errors disappear once you add the remaining functions in the next substep.
Help me understand why TypeScript shows errors for functions defined later in the same file.
Add the status bar, report panel, and deactivate function
The final pieces connect everything together. The status bar shows a live issue count, the report panel gives a detailed breakdown, and the deactivate function cleans up when the extension unloads.
- Below scanVariables, add the updateStatusBar function:
function updateStatusBar(count: number): void {
if (count > 0) {
statusBarItem.text = `🪲 ${count} dead code issue${count !== 1 ? 's' : ''}`;
} else {
statusBarItem.text = '🪲 No dead code';
}
statusBarItem.show();
}
What does this function do?
This updates the status bar text in the bottom-right corner of VS Code. It shows the count of dead code issues found, or a clean "No dead code" message when the file is clear.
You won't see a change yet. The report panel function is next.
- Below updateStatusBar, add the showReport function:
function showReport(): void {
// Create a side-by-side webview panel
const panel = vscode.window.createWebviewPanel(
'deadCodeReport',
'Dead Code Report',
vscode.ViewColumn.Beside,
{ enableScripts: false }
);
// Build HTML table rows from current issues
const rows = currentIssues
.map(
(issue, i) =>
`<tr><td>${i + 1}</td><td>Line ${issue.range.start.line + 1}</td><td>${issue.message}</td></tr>`
)
.join('');
// Set the panel's HTML content
panel.webview.html = `<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; padding: 16px; background: #1e1e1e; color: #d4d4d4; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px 12px; border-bottom: 1px solid #333; text-align: left; }
th { background: #2d2d2d; color: #ff6b6b; }
tr:hover td { background: #2a2a2a; }
h1 { color: #ff6b6b; }
</style>
</head>
<body>
<h1>🪲 Dead Code Report</h1>
<p>${currentIssues.length} issue${currentIssues.length !== 1 ? 's' : ''} found</p>
<table>
<thead><tr><th>#</th><th>Location</th><th>Issue</th></tr></thead>
<tbody>${rows || '<tr><td colspan="3">No issues found!</td></tr>'}</tbody>
</table>
</body>
</html>`;
}
What does this function do?
This opens a Webview panel beside the current editor. The panel displays a styled HTML table listing every dead code issue with its line number and description. Since it is static HTML only, enableScripts is set to false for security.
You won't see a change yet. One last function to add.
- Below showReport, add the deactivate function at the very end of the file:
export function deactivate(): void {
// Clean up all resources when the extension unloads
if (deadDecorationType) {
deadDecorationType.dispose();
}
if (statusBarItem) {
statusBarItem.dispose();
}
if (debounceTimer) {
clearTimeout(debounceTimer);
}
}
What does this function do?
The deactivate function is VS Code's cleanup hook. It disposes the decoration type and status bar item, and clears any pending debounce timer. This prevents memory leaks when the extension is disabled or VS Code shuts down.
- Save src/extension.ts.
- Verify the file compiles by running this command in your terminal:
npm run compile
You should see no errors in the terminal output. TypeScript compiles your code into the out/ folder.
Seeing compile errors?
Check that your file is saved as src/extension.ts (inside the src folder, not the project root). The tsconfig.json expects source files inside src/.
If you see "Cannot find module 'vscode'", make sure you ran npm install in the previous step to install @types/vscode.
Help me debug my TypeScript compile errors in the Dead Code Hunter extension.
✔️ Awesome, I've got everything!
Great. Double check you have saved src/extension.ts and that npm run compile completes with no errors.
ⓧ I'd like to double check the full code
Here is the complete src/extension.ts file. Compare your code against this:
import * as vscode from 'vscode';
interface DeadCodeIssue {
range: vscode.Range;
message: string;
}
let deadDecorationType: vscode.TextEditorDecorationType;
let statusBarItem: vscode.StatusBarItem;
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
let currentIssues: DeadCodeIssue[] = [];
export function activate(context: vscode.ExtensionContext): void {
updateDecorationStyle();
statusBarItem = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Right,
100
);
statusBarItem.command = 'deadCodeHunter.showReport';
context.subscriptions.push(statusBarItem);
const huntCommand = vscode.commands.registerCommand(
'deadCodeHunter.hunt',
() => {
const editor = vscode.window.activeTextEditor;
if (editor) {
scanDocument(editor);
}
}
);
const clearCommand = vscode.commands.registerCommand(
'deadCodeHunter.clear',
() => {
const editor = vscode.window.activeTextEditor;
if (editor) {
editor.setDecorations(deadDecorationType, []);
currentIssues = [];
updateStatusBar(0);
}
}
);
const reportCommand = vscode.commands.registerCommand(
'deadCodeHunter.showReport',
() => {
showReport();
}
);
context.subscriptions.push(huntCommand, clearCommand, reportCommand);
vscode.window.onDidChangeActiveTextEditor(
(editor) => {
if (editor) {
scanDocument(editor);
}
},
null,
context.subscriptions
);
vscode.workspace.onDidChangeTextDocument(
(event) => {
const config = vscode.workspace.getConfiguration('deadCodeHunter');
if (!config.get<boolean>('enableRealtime')) {
return;
}
const editor = vscode.window.activeTextEditor;
if (editor && event.document === editor.document) {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = setTimeout(() => {
scanDocument(editor);
}, config.get<number>('debounceDelay') ?? 500);
}
},
null,
context.subscriptions
);
vscode.workspace.onDidSaveTextDocument(
(document) => {
const config = vscode.workspace.getConfiguration('deadCodeHunter');
if (!config.get<boolean>('enableOnSave')) {
return;
}
const editor = vscode.window.activeTextEditor;
if (editor && editor.document === document) {
scanDocument(editor);
}
},
null,
context.subscriptions
);
vscode.workspace.onDidChangeConfiguration(
(event) => {
if (event.affectsConfiguration('deadCodeHunter')) {
updateDecorationStyle();
const editor = vscode.window.activeTextEditor;
if (editor) {
scanDocument(editor);
}
}
},
null,
context.subscriptions
);
if (vscode.window.activeTextEditor) {
scanDocument(vscode.window.activeTextEditor);
}
}
function updateDecorationStyle(): void {
if (deadDecorationType) {
deadDecorationType.dispose();
}
const config = vscode.workspace.getConfiguration('deadCodeHunter');
const highlightColor = config.get<string>('highlightColor') ?? 'rgba(255, 80, 80, 0.10)';
const opacity = config.get<string>('opacity') ?? '0.35';
deadDecorationType = vscode.window.createTextEditorDecorationType({
opacity: opacity,
backgroundColor: highlightColor,
textDecoration: 'underline wavy rgba(255, 80, 80, 0.7)',
overviewRulerColor: 'rgba(255, 80, 80, 0.8)',
overviewRulerLane: vscode.OverviewRulerLane.Right,
});
}
function scanDocument(editor: vscode.TextEditor): void {
const document = editor.document;
const languageId = document.languageId;
const text = document.getText();
const config = vscode.workspace.getConfiguration('deadCodeHunter');
let issues: DeadCodeIssue[] = [];
const jsLangs = ['javascript', 'javascriptreact', 'typescript', 'typescriptreact'];
const cssLangs = ['css', 'scss', 'less'];
const htmlLangs = ['html', 'vue'];
if (jsLangs.includes(languageId)) {
if (config.get<boolean>('scanStyleObjects')) {
issues = issues.concat(scanStyleObjects(text, document));
}
if (config.get<boolean>('scanVariables')) {
issues = issues.concat(scanVariables(text, document));
}
}
if (cssLangs.includes(languageId)) {
if (config.get<boolean>('scanCssClasses')) {
issues = issues.concat(scanCssClasses(text, document));
}
}
if (htmlLangs.includes(languageId)) {
if (config.get<boolean>('scanCssClasses')) {
issues = issues.concat(scanCssClasses(text, document));
}
if (config.get<boolean>('scanVariables')) {
issues = issues.concat(scanVariables(text, document));
}
}
currentIssues = issues;
const decorations: vscode.DecorationOptions[] = issues.map((issue) => ({
range: issue.range,
hoverMessage: new vscode.MarkdownString(`🪲 **Dead Code:** ${issue.message}`),
}));
editor.setDecorations(deadDecorationType, decorations);
updateStatusBar(issues.length);
}
function scanStyleObjects(text: string, document: vscode.TextDocument): DeadCodeIssue[] {
const issues: DeadCodeIssue[] = [];
const styleBlockRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:StyleSheet\.create\()?{([^}]*)}/gs;
let blockMatch: RegExpExecArray | null;
while ((blockMatch = styleBlockRegex.exec(text)) !== null) {
const varName = blockMatch[1];
const blockContent = blockMatch[2];
const blockStart = blockMatch.index + blockMatch[0].indexOf(blockContent);
const keyRegex = /(\w+)\s*:/g;
let keyMatch: RegExpExecArray | null;
while ((keyMatch = keyRegex.exec(blockContent)) !== null) {
const keyName = keyMatch[1];
const usagePattern = new RegExp(
`${varName}\.${keyName}|${varName}\\[['"\`]${keyName}['"\`]\\]`,
'g'
);
const textBeforeBlock = text.substring(0, blockMatch.index);
const textAfterBlock = text.substring(blockMatch.index + blockMatch[0].length);
const outsideText = textBeforeBlock + textAfterBlock;
if (!usagePattern.test(outsideText)) {
const absolutePos = blockStart + keyMatch.index;
const startPos = document.positionAt(absolutePos);
const endPos = document.positionAt(absolutePos + keyName.length);
issues.push({
range: new vscode.Range(startPos, endPos),
message: `Style key "${keyName}" is never referenced as ${varName}.${keyName}`,
});
}
}
}
return issues;
}
function scanCssClasses(text: string, document: vscode.TextDocument): DeadCodeIssue[] {
const issues: DeadCodeIssue[] = [];
const classRegex = /\.([a-zA-Z_][\w-]*)\s*{/g;
let match: RegExpExecArray | null;
while ((match = classRegex.exec(text)) !== null) {
const className = match[1];
const usagePattern = new RegExp(
`["'\`\\s]${className}["'\`\\s]|class=["'][^"']*\\b${className}\\b[^"']*["']`,
'g'
);
const textWithoutDeclaration =
text.substring(0, match.index) +
' '.repeat(match[0].length) +
text.substring(match.index + match[0].length);
if (!usagePattern.test(textWithoutDeclaration)) {
const dotPos = match.index;
const startPos = document.positionAt(dotPos);
const endPos = document.positionAt(dotPos + match[0].length - 1);
issues.push({
range: new vscode.Range(startPos, endPos),
message: `CSS class ".${className}" is never referenced in this file`,
});
}
}
return issues;
}
function scanVariables(text: string, document: vscode.TextDocument): DeadCodeIssue[] {
const issues: DeadCodeIssue[] = [];
const varRegex = /(?:const|let|var)\s+([a-zA-Z_$][\w$]*)\s*=/g;
let match: RegExpExecArray | null;
while ((match = varRegex.exec(text)) !== null) {
const varName = match[1];
if (varName === 'styles' || varName === 'module' || varName === 'exports') {
continue;
}
const afterDeclaration = text.substring(match.index + match[0].length);
const usagePattern = new RegExp(`\\b${varName}\\b`);
if (!usagePattern.test(afterDeclaration)) {
const nameStart = match.index + match[0].indexOf(varName);
const startPos = document.positionAt(nameStart);
const endPos = document.positionAt(nameStart + varName.length);
issues.push({
range: new vscode.Range(startPos, endPos),
message: `Variable "${varName}" is declared but never read`,
});
}
}
return issues;
}
function updateStatusBar(count: number): void {
if (count > 0) {
statusBarItem.text = `🪲 ${count} dead code issue${count !== 1 ? 's' : ''}`;
} else {
statusBarItem.text = '🪲 No dead code';
}
statusBarItem.show();
}
function showReport(): void {
const panel = vscode.window.createWebviewPanel(
'deadCodeReport',
'Dead Code Report',
vscode.ViewColumn.Beside,
{ enableScripts: false }
);
const rows = currentIssues
.map(
(issue, i) =>
`<tr><td>${i + 1}</td><td>Line ${issue.range.start.line + 1}</td><td>${issue.message}</td></tr>`
)
.join('');
panel.webview.html = `<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; padding: 16px; background: #1e1e1e; color: #d4d4d4; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px 12px; border-bottom: 1px solid #333; text-align: left; }
th { background: #2d2d2d; color: #ff6b6b; }
tr:hover td { background: #2a2a2a; }
h1 { color: #ff6b6b; }
</style>
</head>
<body>
<h1>🪲 Dead Code Report</h1>
<p>${currentIssues.length} issue${currentIssues.length !== 1 ? 's' : ''} found</p>
<table>
<thead><tr><th>#</th><th>Location</th><th>Issue</th></tr></thead>
<tbody>${rows || '<tr><td colspan="3">No issues found!</td></tr>'}</tbody>
</table>
</body>
</html>`;
}
export function deactivate(): void {
if (deadDecorationType) {
deadDecorationType.dispose();
}
if (statusBarItem) {
statusBarItem.dispose();
}
if (debounceTimer) {
clearTimeout(debounceTimer);
}
}
Your extension engine is complete and compiling cleanly. Next up, you will test it live by launching the Extension Development Host and watching it highlight dead code in real-time.
Test Your Extension
You have a complete extension engine with three scanners, real-time detection, and a status bar item.
Now it is time to see it in action. You will compile your TypeScript into JavaScript, create a test file loaded with intentional dead code, and launch the Extension Development Host to watch Dead Code Hunter highlight issues in real-time.
In this step, get ready to:
- Create a test file with intentional dead code to trigger all scanners.
- Compile the TypeScript source and launch the Extension Development Host.
- Verify decorations appear on unused style keys, unused variables, the status bar, and the overview ruler.
Create a test file with intentional dead code
Your extension needs a file with a mix of used and unused code to prove each scanner works correctly. You will create a JavaScript file that includes unused style object keys, unused variables, and used references that should NOT be flagged.
- Create a new file called sample-test.js in the root of your dead-code-hunter folder (the same level as package.json).
- Paste the following code into sample-test.js:
// === Dead Code Hunter Test File ===
// Open this file in the Extension Development Host to see dead code highlighted.
const styles = {
container: { flex: 1, padding: 16 },
ghostButton: { opacity: 0.5 },
title: { fontSize: 24, fontWeight: 'bold' },
unusedCard: { borderRadius: 8 }
};
// Used variables
const usedVar = 42;
const anotherUsed = 'hello';
// Dead variables (never read after declaration)
const deadVar = 'I am never read';
const neverUsed = 100;
// Usage of styles and variables
console.log(styles.container);
console.log(styles.title);
console.log(usedVar + anotherUsed);
What does this file test?
This file is designed to trigger two of your three scanners simultaneously:
- ghostButton and unusedCard are style keys that are never referenced as styles.ghostButton or styles.unusedCard anywhere outside the object. The style object scanner will catch these.
- deadVar and neverUsed are declared but never read after their declaration. The variable scanner will catch these.
- container, title, usedVar, and anotherUsed are all referenced in the console.log lines at the bottom. These should NOT be flagged.
- Save sample-test.js.
Compile and launch the Extension Development Host
Your extension is written in TypeScript, but VS Code runs JavaScript. The compile step converts your src/extension.ts into out/extension.js so VS Code can load it.
- Compile your TypeScript by running this command in the VS Code terminal:
npm run compile
- You should see no errors in the terminal output. A new out/ folder appears in your project containing extension.js and extension.js.map.
Seeing TypeScript errors?
If the compiler reports errors, check that your src/extension.ts file matches the code from the previous step exactly. Common issues include missing closing braces or mismatched parentheses.
Also make sure you ran npm install in an earlier step. The compiler needs @types/vscode and @types/node installed in node_modules to resolve type definitions.
Help me debug TypeScript compile errors in my VS Code extension
Now you will launch a second instance of VS Code called the Extension Development Host. This special window loads your extension so you can test it live.
- Press F5 on your keyboard.
What is the Extension Development Host?
When you press F5, VS Code reads your .vscode/launch.json and opens a brand new VS Code window with your extension pre-loaded. This is the Extension Development Host.
Your original window stays open as the debugger. The new window is where you test your extension, just like any user would experience it.
- A new VS Code window opens with [Extension Development Host] in its title bar.
- In the Extension Development Host window, open the sample-test.js file by clicking it in the Explorer sidebar.
Extension Development Host not opening?
If nothing happens when you press F5, make sure you have the dead-code-hunter folder open as the workspace root in VS Code (not a parent folder containing it).
If you see a build task error, confirm that .vscode/tasks.json exists and contains the npm watch task. The launch configuration uses preLaunchTask to compile before launching.
Help me troubleshoot why F5 is not launching the Extension Development Host
Verify decorations appear on dead code
With sample-test.js open in the Extension Development Host, your extension should have already scanned the file automatically.
- Look at the lines containing ghostButton, unusedCard, deadVar, and neverUsed. These should appear faded (reduced opacity) with a red wavy underline.
- Check the status bar in the bottom-right corner of the Extension Development Host. It should display the issue count (e.g. 4 dead code issues).
- Look at the scrollbar on the right side of the editor. You should see small red markers in the overview ruler indicating where dead code lives in the file.
- Hover over any faded line. A tooltip should appear showing a message like Dead Code: Variable "deadVar" is declared but never read.
- Confirm that container, title, usedVar, and anotherUsed are NOT faded. These are used and should look normal.
Why do some style keys stay normal?
The container and title keys are not flagged because they are referenced as styles.container and styles.title in the console.log lines at the bottom of the file. Your style object scanner checks for usage outside the object declaration.
No decorations appearing?
Make sure you opened sample-test.js in the Extension Development Host window (the new window), not in your original VS Code window. Your extension only runs in the host.
If the file is open in the correct window but nothing is highlighted, check the bottom of the Extension Development Host for an error notification. You can also open the Output panel (View > Output) and select Extension Host from the dropdown to see runtime errors.
Help me figure out why my Dead Code Hunter extension is not showing decorations
✔️ Awesome, I've got everything!
Great. Make sure you have saved sample-test.js and confirmed the decorations are visible in the Extension Development Host.
ⓧ I'd like to double check the full code
Here is the complete sample-test.js file:
// === Dead Code Hunter Test File ===
// Open this file in the Extension Development Host to see dead code highlighted.
const styles = {
container: { flex: 1, padding: 16 },
ghostButton: { opacity: 0.5 },
title: { fontSize: 24, fontWeight: 'bold' },
unusedCard: { borderRadius: 8 }
};
// Used variables
const usedVar = 42;
const anotherUsed = 'hello';
// Dead variables (never read after declaration)
const deadVar = 'I am never read';
const neverUsed = 100;
// Usage of styles and variables
console.log(styles.container);
console.log(styles.title);
console.log(usedVar + anotherUsed);
Your extension is scanning files and highlighting dead code in real-time. Next up, you will add a report panel that lists all issues in a clean table, plus polish the extension with documentation files.
Add the Report Panel and Polish
Your extension now detects dead code and highlights it in real-time. The next thing a developer needs is a summary view that lists everything in one place.
In this step, you will test the report panel command that is already built into your extension, then add professional documentation files that describe your extension's features and version history.
In this step, get ready to:
- Test the "Show Dead Code Report" command in the Extension Development Host.
- Create a README.md with features, commands, configuration, and supported languages.
- Create a CHANGELOG.md documenting the initial release.
Test the Dead Code Report panel
Your showReport() function is already wired up to the deadCodeHunter.showReport command. It creates a Webview panel beside the editor that renders a styled HTML table of all detected issues.
- Press F5 in VS Code to launch the Extension Development Host.
- In the new VS Code window that opens, open the sample-test.js file from the Explorer sidebar.
- Wait a moment for the dead code highlights to appear (faded text, red underlines).
- Open the Command Palette with Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows).
- Type Dead Code Hunter: Show Dead Code Report and press Enter.
A new panel opens beside your editor titled "Dead Code Report". You should see a dark-themed HTML table listing each dead code issue with a row number, line location, and description.
How does the report panel work?
The showReport() function uses vscode.window.createWebviewPanel() to open a side panel with custom HTML. It reads from the currentIssues array (populated by the scanners) and maps each issue into a table row.
The panel uses enableScripts: false because it renders static HTML only. No JavaScript runs inside the Webview, which keeps it secure and lightweight.
Click the status bar too
You can also open the report by clicking the bug icon in the bottom-right status bar. The status bar item's command property is set to deadCodeHunter.showReport, so clicking it triggers the same report panel.
Report panel shows 0 issues?
Make sure you have sample-test.js open and focused as the active editor tab before running the command. The report reads from whichever file is active.
If highlights are not appearing at all, recompile by stopping the Extension Development Host, running npm run compile in the terminal, and pressing F5 again.
Why is my Dead Code Report showing zero issues even though sample-test.js is open?
Create the README
A good README helps other developers (and future you) understand what the extension does, how to use its commands, and what settings are available.
- Create a new file called README.md in the root of your dead-code-hunter folder.
- Paste the following content into the file:
# Dead Code Hunter 🪲
A VS Code extension that fades out unused style properties, CSS classes, and variables — entirely offline, no API keys needed.
## Features
- **Style Object Scanner** — Detects unused keys in `const styles = {}` and `StyleSheet.create({})`
- **CSS Class Scanner** — Flags `.className {}` declarations never referenced
- **Variable Scanner** — Highlights `const`/`let`/`var` declarations never read
- **Real-time Scanning** — Scans as you type with configurable debounce
- **Dead Code Report** — Side-by-side table of all issues
- **Overview Ruler** — Red indicators on the scrollbar
## Commands
Open Command Palette (`Ctrl+Shift+P`) and search for "Dead Code Hunter":
| Command | Description |
|---------|-------------|
| Hunt Dead Code | Manual scan trigger |
| Show Dead Code Report | Opens report panel |
| Clear Dead Code Highlights | Removes all highlights |
## Configuration
Search for "Dead Code Hunter" in Settings, or add to `settings.json`:
```json
{
"deadCodeHunter.enableOnSave": true,
"deadCodeHunter.enableRealtime": true,
"deadCodeHunter.debounceDelay": 500,
"deadCodeHunter.highlightColor": "rgba(255, 80, 80, 0.10)",
"deadCodeHunter.opacity": "0.35",
"deadCodeHunter.scanStyleObjects": true,
"deadCodeHunter.scanCssClasses": true,
"deadCodeHunter.scanVariables": true
}
```
## Supported Languages
| Language | Scanners |
|----------|----------|
| JavaScript / JSX | Style Objects, Variables |
| TypeScript / TSX | Style Objects, Variables |
| CSS / SCSS / Less | CSS Classes |
| HTML / Vue | CSS Classes, Variables |
What does this README cover?
- The Features section lists each scanner and capability so users know what they are installing.
- The Commands table maps the three commands registered in package.json to plain-English descriptions.
- The Configuration section shows all user-facing settings with their defaults, matching the contributes.configuration block in your manifest.
- The Supported Languages table helps users know which file types the extension scans.
- Save README.md.
- Confirm the file appears in the Explorer sidebar under your dead-code-hunter folder.
✔️ Awesome, I've got everything!
Great. Make sure you have saved your file before moving on.
ⓧ I'd like to double check the full code
# Dead Code Hunter 🪲
A VS Code extension that fades out unused style properties, CSS classes, and variables — entirely offline, no API keys needed.
## Features
- **Style Object Scanner** — Detects unused keys in `const styles = {}` and `StyleSheet.create({})`
- **CSS Class Scanner** — Flags `.className {}` declarations never referenced
- **Variable Scanner** — Highlights `const`/`let`/`var` declarations never read
- **Real-time Scanning** — Scans as you type with configurable debounce
- **Dead Code Report** — Side-by-side table of all issues
- **Overview Ruler** — Red indicators on the scrollbar
## Commands
Open Command Palette (`Ctrl+Shift+P`) and search for "Dead Code Hunter":
| Command | Description |
|---------|-------------|
| Hunt Dead Code | Manual scan trigger |
| Show Dead Code Report | Opens report panel |
| Clear Dead Code Highlights | Removes all highlights |
## Configuration
Search for "Dead Code Hunter" in Settings, or add to `settings.json`:
```json
{
"deadCodeHunter.enableOnSave": true,
"deadCodeHunter.enableRealtime": true,
"deadCodeHunter.debounceDelay": 500,
"deadCodeHunter.highlightColor": "rgba(255, 80, 80, 0.10)",
"deadCodeHunter.opacity": "0.35",
"deadCodeHunter.scanStyleObjects": true,
"deadCodeHunter.scanCssClasses": true,
"deadCodeHunter.scanVariables": true
}
```
## Supported Languages
| Language | Scanners |
|----------|----------|
| JavaScript / JSX | Style Objects, Variables |
| TypeScript / TSX | Style Objects, Variables |
| CSS / SCSS / Less | CSS Classes |
| HTML / Vue | CSS Classes, Variables |
Create the CHANGELOG
A changelog documents what changed in each version. The VS Code Marketplace displays this file on your extension's page, and it helps users decide whether to update.
- Create a new file called CHANGELOG.md in the root of your dead-code-hunter folder.
- Paste the following content into the file:
# Changelog
## [0.1.0] - 2026-06-02
### Added
- Style object scanner for `const styles = {}` and `StyleSheet.create({})`
- CSS class scanner for unused `.className {}` declarations
- Variable scanner for unused `const`/`let`/`var` declarations
- Real-time scanning with configurable debounce delay
- Dead Code Report webview panel
- Status bar item showing issue count
- Overview ruler indicators
- Configurable highlight color and opacity
What does this CHANGELOG document?
This records every feature you built in version 0.1.0. Each bullet matches a capability in your extension: the three scanners, real-time mode, the report panel, the status bar, overview ruler markers, and configurable styling.
- Save CHANGELOG.md.
- Confirm the file appears in the Explorer sidebar alongside README.md.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before continuing.
ⓧ I'd like to double check the full code
# Changelog
## [0.1.0] - 2026-06-02
### Added
- Style object scanner for `const styles = {}` and `StyleSheet.create({})`
- CSS class scanner for unused `.className {}` declarations
- Variable scanner for unused `const`/`let`/`var` declarations
- Real-time scanning with configurable debounce delay
- Dead Code Report webview panel
- Status bar item showing issue count
- Overview ruler indicators
- Configurable highlight color and opacity
Your extension is fully functional and documented. Next up, you will take it one step further and publish it to the VS Code Marketplace so anyone in the world can install it.
Secret mission
Publish to the VS Code Marketplace
Your extension works locally, but what if anyone in the world could install it with one click? In this secret mission, you'll publish Dead Code Hunter to the VS Code Marketplace so other developers can find and install it directly from their editor.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The dead-code-hunter project folder on your Desktop.
- The @vscode/vsce global npm package.
- Your published extension on the VS Code Marketplace (if you completed the Secret Mission).
Keep everything running
No action needed. Choose this if you're still actively building or want to keep the extension available on the Marketplace.
- Your dead-code-hunter folder stays on your Desktop and can be opened in VS Code anytime.
- The @vscode/vsce global package remains installed for future extension publishing.
- Your Microsoft/Azure DevOps account and Publisher profile remain active at no cost.
- Your extension remains on the Marketplace for anyone to install (if published).
Pause - I'll come back to this later
Close VS Code and any Extension Development Host windows. Everything stays on disk for when you return.
- Close any open Extension Development Host windows by closing the second VS Code window that launched with F5.
- Close your terminal if the TypeScript watcher is still running.
- All project files remain in the dead-code-hunter folder. Reopen it in VS Code when you're ready to continue.
Delete - I don't want to use this again
Remove all project resources from your machine. This is irreversible.
- If you published to the Marketplace, remove your extension by running:
vsce unpublish publisher-name.dead-code-hunter
What does this command do?
Replace publisher-name with your actual publisher name. The format is publisher.extension-name. This permanently removes the extension from the Marketplace.
- Remove the stored publisher credentials by running:
vsce logout publisher-name
- Uninstall the @vscode/vsce global package by running:
npm uninstall -g @vscode/vsce
- Delete the project folder (macOS/Linux) by running:
cd ~/Desktop
rm -rf dead-code-hunter
- Delete the project folder (Windows) by running:
cd ~\Desktop
rmdir /s /q dead-code-hunter
- Confirm the folder is gone by running:
ls
You should no longer see dead-code-hunter in the output.
Nice Work!
Nice Work!
Nice work! You just built a professional TypeScript VS Code extension from scratch that detects and visually highlights dead code in real-time.
You've learned how to:
- Built a multi-scanner code analysis engine using regex pattern matching with full TypeScript type safety.
- Used the VS Code Decoration API to visually highlight dead code with reduced opacity, wavy underlines, and overview ruler markers.
- Created a complete extension manifest with commands, user-facing configuration, activation events, and a TypeScript build pipeline.
- Published the extension to the VS Code Marketplace for the world to install and use.
Ready to quiz yourself?