Build a Vue Click-to-VSCode Loader & Plugin: Step‑by‑Step Guide
Learn how to create a custom webpack loader and plugin that injects source‑code metadata into Vue components, enabling click‑to‑open functionality in VSCode, with detailed explanations of loader and plugin architecture, debugging techniques, server setup, and integration steps for a seamless development experience.
Meta
Abstract
This article demonstrates a practical implementation of a webpack loader and plugin that enables clicking a Vue page element to jump to the corresponding VSCode source code, providing a simple introduction to loader and plugin development.
Audience Benefits
Readers will gain a clearer understanding of webpack loaders and plugins, learn how to develop them, and also pick up related Vue, CSS, and Node knowledge.
Effect
Result preview:
Source Repository
https://github.com/zh-lx/vnode-loader
https://github.com/zh-lx/vnode-plugin
Prerequisite Knowledge
Loader
Purpose
A webpack loader converts files of various types into modules that webpack can process, allowing non‑JS assets to be handled and optionally transformed.
Structure
A loader exports a JavaScript function that webpack calls via its loader runner, receiving the previous loader’s result or the raw resource.
// synchronous loader example
module.exports = function (content, map, meta) {
return someSyncOperation(content);
};
// asynchronous loader example
module.exports = function (content, map, meta) {
const callback = this.async();
someAsyncOperation(content, (err, result) => {
if (err) return callback(err);
callback(null, result, map, meta);
});
};Plugin
Purpose
Plugins extend webpack functionality beyond what loaders can achieve.
Structure
Export a named function or class.
Define an apply method on its prototype.
Tap into a webpack compiler hook.
Manipulate internal webpack data.
Invoke webpack callbacks when done.
// simple plugin example
class MyExampleWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('MyExampleWebpackPlugin', (compilation, callback) => {
console.log('Example plugin triggered');
// custom processing here
callback();
});
}
}
module.exports = MyExampleWebpackPlugin;Overall Idea
Open VSCode from the browser using a Node server and the launchEditor utility (which uses child_process).
Inject source‑code location information into the DOM via a custom loader, so each element knows its file, line, and column.
Implementation Process
vnode‑loader
Debugging with loader‑runner
Use loader-runner to run a loader in isolation, avoiding full webpack rebuilds.
const { runLoaders } = require('loader-runner');
const fs = require('fs');
const path = require('path');
runLoaders({
resource: path.resolve(__dirname, './src/App.vue'),
loaders: [path.resolve(__dirname, './node_modules/vnode-loader')],
context: { minimize: true },
readResource: fs.readFile.bind(fs)
}, (err, res) => {
if (err) console.log(err);
console.log(res);
});Vue‑CLI integration
Add a .vscode/launch.json and a debug script in package.json to launch the dev server with Node inspector.
{
"version": "0.2.0",
"configurations": [{
"type": "node",
"request": "launch",
"name": "debug",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "debug"],
"port": 5858
}]
} {
"scripts": {
"debug": "node --inspect-brk=5858 ./node_modules/@vue/cli-service/bin/vue-cli-service.js serve"
}
}Template parsing
Parse the .vue file’s template using @vue/compiler-sfc, locate the AST, and replace the original source with an injected version that contains file, line, column, and node name attributes.
import { parse } from '@vue/compiler-sfc';
export = function TrackCodeLoader(content) {
const filePath = this.resourcePath;
const params = new URLSearchParams(this.resource);
if (params.get('type') === 'template') {
const { descriptor } = parse(content);
const domAst = descriptor.template.ast;
const source = domAst.loc.source;
const newSource = getInjectContent(domAst, source, filePath);
return content.replace(source, newSource);
}
return content;
};vnode‑plugin
Node server to launch VSCode
import http from 'http';
import portFinder from 'portfinder';
import launchEditor from './launch-editor';
let started = false;
export = function StartServer(callback) {
if (started) return;
started = true;
const server = http.createServer((req, res) => {
const params = new URLSearchParams(req.url.slice(1));
const file = params.get('file');
const line = Number(params.get('line'));
const column = Number(params.get('column'));
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': 'Content-Type,XFILENAME,XFILENAMECATEGORY,XFILESIZE,X-URL-PATH,x-access-token'
});
res.end('ok');
launchEditor(file, line, column);
});
portFinder.getPort({ port: 4000 }, (err, port) => {
if (err) throw err;
server.listen(port, () => callback(port));
});
};Feature toggle UI
A floating draggable control toggles the tracking feature.
<div id="_vc-control-suspension" draggable="true">V</div> let is_tracking = false;
const ctrl = document.getElementById('_vc-control-suspension');
ctrl.addEventListener('click', function (e) {
if (is_tracking) {
is_tracking = false;
ctrl.style.backgroundColor = 'gray';
} else {
is_tracking = true;
ctrl.style.backgroundColor = 'lightgreen';
}
});Mouse overlay
When tracking is enabled, a fixed overlay shows the DOM element’s tag, file path, and class list.
window.addEventListener('mousemove', function (e) {
if (!is_tracking) return;
const path = e.path;
let target;
for (const node of path) {
if (node.hasAttribute && node.hasAttribute('__FILE__')) {
target = node;
break;
}
}
if (target) setCover(target);
});Click handling
Capture clicks in the capture phase, stop propagation, and send a request to the local server to open VSCode at the injected location.
window.addEventListener('click', function (e) {
if (!is_tracking) return;
const path = e.path;
let target;
for (const node of path) {
if (node.hasAttribute && node.hasAttribute('__FILE__')) {
target = node;
break;
}
}
if (target) {
e.stopPropagation();
e.preventDefault();
const file = target.getAttribute('__FILE__');
const line = target.getAttribute('__LINE__');
const column = target.getAttribute('__COLUMN__');
const url = `http://localhost:__PORT__/?file=${file}&line=${line}&column=${column}`;
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.send();
}
}, true);Injecting code into HTML
Use html-webpack-plugin ’s htmlWebpackPluginAfterHtmlProcessing hook to append the generated script and start the server.
import startServer from './server';
import injectCode from './get-inject-code';
class TrackCodePlugin {
apply(compiler) {
compiler.hooks.compilation.tap('TrackCodePlugin', compilation => {
startServer(port => {
const code = injectCode(port);
compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tap('HtmlWebpackPlugin', data => {
data.html = data.html.replace('</body>', `${code}
</body>`);
});
});
});
}
}
export = TrackCodePlugin;Integration Steps
Install the loader and plugin: yarn add vnode-loader vnode-plugin -D Configure vue.config.js to use the loader and plugin in development mode.
Add a .env.local file with VUE_EDITOR=code to specify VSCode as the editor.
Enable the code command in your terminal (Command + Shift + P → “Install ‘code’ command in PATH”).
Performance
Benchmarks on large projects show negligible impact on webpack build and rebuild times after adding the loader and plugin.
Conclusion
This guide provides a solid foundation for developing custom webpack loaders and plugins, illustrating how they can be leveraged to create powerful development‑time features such as click‑to‑open source code directly from the browser.
References
Webpack loader runner: https://github.com/webpack/loader-runner
Webpack compiler hooks: https://webpack.docschina.org/api/compiler-hooks/
Vue compiler‑sfc: https://github.com/vuejs/vue-next/tree/master/packages/compiler-sfc#readme
launchEditor utility: https://github.com/facebook/create-react-app/blob/main/packages/react-dev-utils/launchEditor.js
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
