A few years ago, I discovered the power of Chrome and VSCode extensions in improving productivity. I created my own extension if my desired feature was not available. The extension systems allow me to experiment my wild ideas with less effort thanks to their graceful design.
While they have similarities, there are also some significant differences determined by the nature of each app. In this blog post, I will provide an overview of the extension systems of Chrome and VSCode and share what I have learned from them.
Development
Initialization
Both extension systems have an entry point - a JSON file. Chrome extension starts from a manifest.json file, while VSCode extensions extend the package.json file, which can sometimes become lengthy and contain too much information. I am worried about conflicts arise from the file.
In the JSON, developer need to provide essential information such as icon for display and script to initialise with. Each parts of a Chrome extension require a script file or a html file, while VSCode only need an entry script.
Here is an example of a manifest.json
file for a Chrome extension:
{
"name": "Broken Background Color",
"version": "1.0",
"description": "Fix an Extension!",
"permissions": ["activeTab", "scripting", "storage"],
"options_page": "options.html",
"background": {
"service_worker": "service-worker.js"
},
"icons": {
"16": "images/icon-16.png"
},
"manifest_version": 3
}
And here is an example of a package.json
file for a VSCode extension:
{
"name": "cat-coding",
"description": "Cat Coding - A Webview API Sample",
"version": "0.0.1",
"publisher": "vscode-samples",
"activationEvents": ["*"],
"main": "./out/extension.js",
"contributes": { ... },
"devDependencies": { ... }
}
In addition to these files, Chrome extensions also require careful declaration of browser permissions to enable their APIs. I totally understand the importance and difficulty of guarding browser security, but providing excessive information that can be accessed through automation is unreasonable.
APIs
The extension APIs in both Chrome and VSCode are well-categorized based on the functionality of the app. This categorization allows developers to explore the possibilities by following the names of the app modules. For example:
chrome.cookies.get({ url: "https://example.com", name: "cookie_name" }, (cookie) => {
if (cookie) {
console.log(cookie.value);
}
});
However, there are possibilities that we have no clues about the name at all ๐คฆโโ๏ธ such as the text decoration in VSCode as mentioned in this blog.
Being a Microsoft product, the coding style of VSCode extensions can sometimes be too OOP style. For instance, to implement a tree view, we must declare a class extends the TreeDataProvider
class and provides methods getTreeItem()
and getChildren()
:
export class DepNodeProvider implements vscode.TreeDataProvider<Dependency> {
private _onDidChangeTreeData: vscode.EventEmitter<Dependency | undefined | void> = new vscode.EventEmitter<Dependency | undefined | void>();
readonly onDidChangeTreeData: vscode.Event<Dependency | undefined | void> = this._onDidChangeTreeData.event;
constructor(private workspaceRoot: string | undefined) {
}
refresh(): void {
this._onDidChangeTreeData.fire();
}
getTreeItem(element: Dependency): vscode.TreeItem {
return element;
}
getChildren(element?: Dependency): Thenable<Dependency[]> {
//...
}
}
Plus the โtrapsโ I mentioned above in Chrome, a well structured and clear document with rich example codes become vital.
The script triggers
There are three ways to trigger our scripts:
- From the extension activation;
- User interaction on the extension interface;
- Internal events.
Activating extension
When a user enables an extension in Chrome, the background script of the extension starts working immediately.
Chrome has migrated the extension background script to the Service Worker in manifest v3, making the extension work more like a web app.
In VSCode, the whole extension initializes from one JS file, and the main JS file need to export an activate()
function. Developers shall declare the activation conditions in the package.json file (see the example above).
// out/extension.js
export function activate(context) { /* ... */ }
It seems that Chrome tries to align the pattern with web development, and VSCode adopts another style commonly seen in serverless development. As a web developer, the way of activating script in the Chrome extensions is more natural to me.
The extension parts and interfaces
A Chrome extension may contain 4 parts of scripts or HTML pages for different purposes:
- a popup page for the popup activated from the menu next to the address bar;
- panel pages in the developer tool;
- a content script to interact with the web page content;
- background scripts or page for more interactions.
They are just web pages or scripts allow to access Chrome APIs with some constrains:
- We must declare the corresponding permissions in the manifest.json;
- Some APIs may only work in certain scope (e.g, we can only access user tab
document
element from the content script).
VSCode defines too many parts to list in a blog (see Contribution Points). Some parts (or we should call contribution points) are so tiny that all they need are displaying texts and actions.
As multiple contribution points often result into the same action, VSCode took a command approach. Command is a scoped key name binding to a registered script.
We may define our own command in the package.json:
{
"contributes": {
"commands": [
{
"command": "myExtension.sayHello",
"title": "Say Hello"
}
]
}
}
And register the command in the activate()
function:
export function activate(context) {
const command = 'myExtension.sayHello';
context.subscriptions.push(
vscode.commands.registerCommand(command, () => console.log('Hello!'))
);
}
We may trigger either internal command or our customised command from its name:
vscode.commands.executeCommand('editor.action.addCommentLine');
Or a Uri (for text editor or webview):
const commandUri = vscode.Uri.parse(`command:editor.action.addCommentLine`)
When everything relies on name, all the problems we had debated for CSS class come across. It may not cause any issue at all when the extension is small, but it is hard to tell when the extension grows big.
In fact, more names and complexities come with the command Taking one of the menu contribution point in my extension as example:
{
"contributes": {
"view/title": [
{
"command": "gitlabSnippets.addHost",
"when": "view =~ /^gitlabSnippetsExplorer-(mine|all)$/",
"group": "navigation"
}
]
}
}
The configuration above adds an icon button to the title of 2 view panels. VSCode requires:
- A when clause expression mixing a bit flavor of bash and regular expressions;
- ID (like the view name โgitlabSnippetsExplorer-mineโ above);
- Position name (like the โgroupโ above).
In return, our design needs get minimized. All we need to provide are the condition to display, the icon and the description text.
In addition, thinking in command forces developers to split part of the workflow and reuse as much as possible. For example, in my GitLab Snippets Explorer extension, there is a addHost
command showing up as a command palette menu item and an icon button in the view panels.
In the meantime, another publishSnippet
command simply jump to this adding host action if user select the โAdding new โฆโ option below:
Events
Both Chrome and VSCode provide a set of events to interact with.
We may subscribe to a Chrome event like following:
chrome.devtools.network.onNavigated.addListener(
callback: function,
)
In VSCode, we may listen to an event with a slightly shorter path, like this:
vscode.workspace.onDidChangeConfiguration(callbackFunction);
We may fire some event manually. For example, in the VSCode TreeView, it is necessary to inform data changes to with this.onDidChangeTreeData.fire()
in the TreeDataProvider
:
export class ExampleTreeProvider implements vscode.TreeDataProvider<Dependency> {
private _onDidChangeTreeData: vscode.EventEmitter<Dependency | undefined | void> = new vscode.EventEmitter<Dependency | undefined | void>();
readonly onDidChangeTreeData: vscode.Event<Dependency | undefined | void> = this._onDidChangeTreeData.event;
refresh(): void {
this._onDidChangeTreeData.fire();
}
// ...
}
This looks ugly and unbelievably long to me (even worse in Typescript). I thought the tedious method binding in the React class component was the worst thing I knew till I see this.
The communication between parts
In Chrome extensions, communication between different parts is achieved through messaging. It is similar to the messaging between browser main thread and workers:
// Send a message
chrome.runtime.sendMessage({ message: 'hello' }, function(response) {
console.log(response);
});
// Receive a message
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.message === 'hello') {
sendResponse({ message: 'hello from background script' });
}
});
Instead of messaging, VSCode extension take advantage of the command design except for webview. I think it may be more reasonable for webview to execute command instead of posting message if the message type is complex.
UI/UX
The command design in VSCode also provides a convenient developer experience and a consistent user experience. Developers only need to provide an icon, label, and description for most scenarios. However, this convenience can sometimes limit creativity and the overall user experience.
In contrast, Chrome extensions offer more freedom for developers in terms of UI/UX design.
Testing
Testing a Chrome extension
Testing a Chrome extension is very similar to testing a web page. There are a few differences:
- You will need to manually load the compiled folder in
chrome://extensions
and update a background script will bring you back there for a manual update. - You have to find the inspection window for a background script from the extension detail page or the service worker list.
- You may have no idea of why things do not work due to a wrong manifest configuration such as permission missing ๐.
- You have to remove the developer version to revive the live installation.
Testing a VSCode extension
Testing a VSCode extension requires more learning efforts.
I confess that I am not motivated in adding automation tests in my small extensions since the VSCode APIs are everywhere.
The manual testing of a VSCode extension runs on the debug system of VSCode. It is my favorite tool of debugging Node.js. Additionally, the test runs on an isolated environment and the marketplace installation settings are not affected.
However, I find it hard to trouble shoot the root cause sometimes as:
- Details of the network requests are invisible.
- Error messages from the message popup or the log panel are not helpful enough, and very little tracing information is offered.
- Passing the local testing does not mean the published one will work perfectly.
Distribution
Distribution of Chrome extension
To distribute a Chrome extension, we need to:
- Register a Google account and top-up $5 to become an eligible Chrome Web Store Developer.
- Pack the extension to a
.zip
manually as required. - Fill out a crazy long form to market the extension and declare the purpose of requesting for the permissions (I mean, each permission).
- Wait for several days to pass or fail the review.
I was horrified by the long form at my first submission. I didnโt expect it would force me to bring out my design tool to get the screenshot size right and the file size small. I thought we have image processing plugins for decades?
What made things worse was that I waited for a long review time due to a public holiday. The result was an short Email of submission failure. It took me some time to figure out the problem was requesting some unnecessary permission and filling in unclear information.
Once successfully submitting the extension, we may see the basic download times and user review comments.
To understand more of the extension usage, it is possible to integrate analytic tools like Google Analytics. I forgot the details but it was not as straightforward as integrating in a normal web app.
Google allow to publish an extension without showing up in their search engine and the web store. User may only access this kind of extension from a URL. This is a perfect simple work around for internal extensions.
Distribution of VSCode extension
In contrast, the process of publishing a VSCode extension is incredibly simple and efficient:
- Register an Azure account and get a personal token with marketplace access.
- Install the latest version of CLI tool
vsce
. -
vsce login
with the account info. - Compile the code and
vsce publish
. - Usually within few hours will receive an Email of success or fail.
Well, I took one more step to summarise the process, butโฆ
- No form fillings for the extension. All the necessary information could be found in the package.json, README.md just like npm do.
-
vsce
does some simple review to fail fast and prevent meaningless waiting. - The review process was so short that I suspect it is mostly automated as well.
However, I did meet a review failure without helpful information due to a โriskyโ API usage. And I also did find my extension not working as expected after publishing due to packing wrongly.
Similar to Chrome extension, once successfully publishing a VSCode extension, we may see the basic download times, user review comments, and a handy statics graph of the recent downloads.
Again, it is far from enough to understand user behaviors and figuring out how to improve from these data. Most of the Node.JS monitoring tools are focusing on Node.JS server, not native tools with interfaces like this. It seems to me that I have to setup the metrics and monitoring system by my own.
For the internal extensions, someone had experimented and created an extension: Private Extension Manager. One of my brilliant teammate did fork and made some small changes to create an internal extension marketplace.
Summary of what I have learnt
First of all, here are the essentials for an extension system based on my observation:
-
An entry point. JSON is perfect for it, but we need to design wisely. I think it is great to reuse some information like
name
,version
,description
from the package.json but there is conflict risk to extend the file designed for another system. - Super clear document about the extension concepts, the APIs, the configuration of extension parts, the distribution guide, etc.
- Isolated runtime environment for the whole extension or even for each part of the extension.
- A communication channel between the parts of the extension. I love the command design from VSCode, but it would be better to avoid trapping us in the naming battle and costing us to learn new expressions.
- APIs categorised by feature, consistent in style and similar to the existing standards to flatten the learning curve.
- App events subscription.
- Complete testing support. It is complicated to make a good one, but we may try to use the mature system from browsers.
- A marketplace for user to discover and developer to publish, analyse the extension usage.
- A review workflow. Automate as much as we can, request as little information as we can.
The enhancement I believe is small but significantly improved developer experience is: publishing tool like vsce
. We may fail faster with it.
It is also nice to have:
- Helpful and traceable error messages.
- Official solution for internal extensions.
- More informative statistics.
And there are dark and bright sides to choose: design freedom or programming efficiency and complex configuration. No matter which choice we make, extracting actions is a great choice for code reusability.
Last but not least, I will try my best to keep away from:
- Asking for declaring permission instead of finding out from the code.
- Names, a lot of names.
- Creating new rule expressions.
Top comments (0)