Search for Web Devs
In this tutorial, we’ll create a plugin that allows you to search for NPM packages and MDN pages.
To create a plugin, run the following command:
npm create @yusyuriv/flow-launcher-plugin
yarn create @yusyuriv/flow-launcher-plugin
pnpm create @yusyuriv/flow-launcher-plugin
bun create @yusyuriv/flow-launcher-plugin
It will ask you a few questions and create a plugin project for you. You should have something like this now:
import {FlowPlugin} from "flow-launcher-extended-plugin";
@FlowPlugin.Classexport class NpmAndMdnPlugin extends FlowPlugin {}
import {FlowPlugin} from "flow-launcher-extended-plugin";
@FlowPlugin.Classexport class NpmAndMdnPlugin extends FlowPlugin {}
Let’s add a response for when no command is selected:
@FlowPlugin.SearchemptyQuery() { return "Type 'npm' or 'mdn' to search for packages or MDN web docs";}
@FlowPlugin.SearchemptyQuery() { return "Type 'npm' or 'mdn' to search for packages or MDN web docs";}
NPM Search
Let’s add a response for when the user types npm
without any package name.
@FlowPlugin.Search({ equalTo: "npm" })emptyNpmQuery() { return "Type 'npm <package-name>' to search for a package";}
@FlowPlugin.Search({ equalTo: "npm" })emptyNpmQuery() { return "Type 'npm <package-name>' to search for a package";}
After googling for NPM search API, I found this documentation , which looks exactly like what we will need for the plugin. We’ll need the package name, version, description, and NPM link.
{ "objects": [ { "package": { "name": "yargs", "version": "6.6.0", "description": "yargs the modern, pirate-themed, successor to optimist.", "keywords": [ "argument", "args", "option", "parser", "parsing", "cli", "command" ], "date": "2016-12-30T16:53:16.023Z", "links": { "npm": "https://www.npmjs.com/package/yargs", "homepage": "http://yargs.js.org/", "repository": "https://github.com/yargs/yargs", "bugs": "https://github.com/yargs/yargs/issues" }, "publisher": { "username": "bcoe", }, "maintainers": [ { "username": "bcoe", }, { "username": "chevex", }, { "username": "nexdrew", }, { "username": "nylen", } ] }, "score": { "final": 0.9237841281241451, "detail": { "quality": 0.9270640902288084, "popularity": 0.8484861649808381, "maintenance": 0.9962706951777409 } }, "searchScore": 100000.914 } ], "total": 1, "time": "Wed Jan 25 2017 19:23:35 GMT+0000 (UTC)"}
Let’s describe this data as types. This is not mandatory, but it’s nice to have your IDE suggest the properties of the object as you’re typing them. Also helps preventing typos in the future.
/** * @typedef {object} NpmResponse * @property {NpmObject[]} objects *//** * @typedef {object} NpmObject * @property {NpmPackage} package *//** * @typedef {object} NpmPackage * @property {string} name * @property {string} version * @property {string} description *//** * @typedef {object} NpmLinks * @property {string} npm */
type NpmResponse = { objects: NpmObject[];}type NpmObject = { package: NpmPackage;}type NpmPackage = { name: string; version: string; description: string; links: NpmLinks;}type NpmLinks = { npm: string;}
Now that we have our data described, let’s add a method to our plugin that will search for NPM packages and display search results.
npmApiUrl = "https://registry.npmjs.com/-/v1/search?text=";
/** @param {Query} query */@FlowPlugin.Search({ startsWith: "npm " })async searchNpmQuery(query) { /** @type {NpmResponse} */ const response = await this.api.httpGetJson(npmApiUrl, query.Search);
return response?.objects.map(/** @param {NpmObject} v */ v => ({ title: `${v.package.name} | v${v.package.version}`, subtitle: v.package.description, action: this.actions.openUrl(v.package.links.npm), }));}
private readonly npmApiUrl = "https://registry.npmjs.com/-/v1/search?text=";
@FlowPlugin.Search({ startsWith: "npm " })async searchNpmQuery(query: Query) { const response = await this.api.httpGetJson<NpmResponse>(npmApiUrl, query.search);
return response?.objects.map(v => ({ title: `${v.package.name} | v${v.package.version}`, subtitle: v.package.description, action: this.actions.openUrl(v.package.links.npm), }));}
We have successfully implemented the NPM search functionality.
Assuming you specified web
as your keyword when creating the plugin, here’s how it should look:
MDN Search
MDN search is a little bit different. We’ll be doing the search ourselves, locally. For that, we’ll need to download the full list of MDN pages.
But before that, let’s add a response for when the user types mdn
without any search query.
@FlowPlugin.Search({ equalTo: "mdn" })emptyMdnQuery() { return "Type 'mdn <search-term>' to search MDN web docs";}
@FlowPlugin.Search({ equalTo: "mdn" })emptyMdnQuery() { return "Type 'mdn <search-term>' to search MDN web docs";}
Now, let’s download the MDN data.
It’s located
here .
This is an array of objects, each object representing an MDN page.
Each object only has two properties: title
and url
. Simple enough. Let’s begin!
First, let’s define the types again. The structure here is much simpler than the NPM data, it’s just one object with two properties.
/** * @typedef {object} MdnArticle * @property {string} title * @property {string} url */
type MdnArticle = { title: string; url: string;}
Now we need to actually download that data. We’ll do this on plugin startup, using the FlowPlugin.Init
decorator:
mdnIndexUrl = "https://developer.mozilla.org/en-US/search-index.json";/** @type {MdnArticle[]} */mdnData = [];
@FlowPlugin.Initasync downloadMdnData() { this.mdnData = await this.api.httpGetJson(mdnIndexUrl);}
private readonly mdnIndexUrl = "https://developer.mozilla.org/en-US/search-index.json";private mdnData: MdnArticle[] = [];
@FlowPlugin.Initasync downloadMdnData() { this.mdnData = await this.api.httpGetJson<MdnArticle[]>(this.mdnIndexUrl);}
Now that we have the data, let’s add a method to our plugin that will search for MDN pages and display search results.
/** @param {Query} query */@FlowPlugin.Search({ startsWith: "mdn " })searchMdnQuery(query) { const search = query.Search.toLowerCase();
return this.mdnData .filter(v => v.title.toLowerCase().includes(search)) .map(v => ({ title: v.title, action: this.actions.openUrl(this.mdnUrlPrefix + v.url), }));}
@FlowPlugin.Search(startsWith: "mdn ")searchMdnQuery(query: Query) { const search = query.search.toLowerCase();
return this.mdnData .filter(v => v.title.toLowerCase().includes(search)) .map(v => ({ title: v.title, action: this.actions.openUrl(this.mdnUrlPrefix + v.url), }));}
Let’s test it out.
It works! We have successfully implemented the MDN search functionality.
The Code
Here’s the full code from this tutorial:
import {FlowPlugin} from "flow-launcher-extended-plugin";
/** * @typedef {object} NpmResponse * @property {NpmObject[]} objects *//** * @typedef {object} NpmObject * @property {NpmPackage} package *//** * @typedef {object} NpmPackage * @property {string} name * @property {string} version * @property {string} description *//** * @typedef {object} NpmLinks * @property {string} npm */
/** * @typedef {object} MdnArticle * @property {string} title * @property {string} url */
@FlowPlugin.Classexport class NpmAndMdnPlugin extends FlowPlugin { npmApiUrl = "https://registry.npmjs.com/-/v1/search?text="; mdnIndexUrl = "https://developer.mozilla.org/en-US/search-index.json"; mdnUrlPrefix = "https://developer.mozilla.org";
/** @type {MdnArticle[]} */ mdnData = [];
@FlowPlugin.Init async downloadMdnData() { this.mdnData = await this.api.httpGetJson(this.mdnIndexUrl); }
@FlowPlugin.Search emptyQuery() { return "Type 'npm' or 'mdn' to search for packages or MDN web docs"; }
@FlowPlugin.Search({ equalTo: "npm" }) emptyNpmQuery() { return "Type 'npm <package-name>' to search for a package"; }
/** @param {Query} query */ @FlowPlugin.Search({ startsWith: "npm " }) async searchNpmQuery(query) { /** @type {NpmResponse} */ const response = await this.api.httpGetJson(this.npmApiUrl, query.Search);
return response?.objects.map(/** @param {NpmObject} v */ v => ({ title: `${v.package.name} | v${v.package.version}`, subtitle: v.package.description, action: this.actions.openUrl(v.package.links.npm), })); }
@FlowPlugin.Search({ equalTo: "mdn" }) emptyMdnQuery() { return "Type 'mdn <search-term>' to search MDN web docs"; }
/** @param {Query} query */ @FlowPlugin.Search({ startsWith: "mdn " }) searchMdnQuery(query) { const search = query.Search.toLowerCase();
return this.mdnData .filter(v => v.title.toLowerCase().includes(search)) .map(v => ({ title: v.title, action: this.actions.openUrl(this.mdnUrlPrefix + v.url), })); }}
import {FlowPlugin} from "flow-launcher-extended-plugin";
type NpmResponse = { objects: NpmObject[];}type NpmObject = { package: NpmPackage;}type NpmPackage = { name: string; version: string; description: string; links: NpmLinks;}type NpmLinks = { npm: string;}
type MdnArticle = { title: string; url: string;}
@FlowPlugin.Classexport class NpmAndMdnPlugin extends FlowPlugin { private readonly npmApiUrl = "https://registry.npmjs.com/-/v1/search?text="; private readonly mdnIndexUrl = "https://developer.mozilla.org/en-US/search-index.json"; private readonly mdnUrlPrefix = "https://developer.mozilla.org";
private mdnData: MdnArticle[] = [];
@FlowPlugin.Init async downloadMdnData() { this.mdnData = await this.api.httpGetJson<MdnArticle[]>(this.mdnIndexUrl); }
@FlowPlugin.Search emptyQuery() { return "Type 'npm' or 'mdn' to search for packages or MDN web docs"; }
@FlowPlugin.Search({ equalTo: "npm" }) emptyNpmQuery() { return "Type 'npm <package-name>' to search for a package"; }
@FlowPlugin.Search({ startsWith: "npm " }) async searchNpmQuery(query: Query) { const response = await this.api.httpGetJson<NpmResponse>(this.npmApiUrl, query.search);
return response?.objects.map(v => ({ title: `${v.package.name} | v${v.package.version}`, subtitle: v.package.description, action: this.actions.openUrl(v.package.links.npm), })); }
@FlowPlugin.Search({ equalTo: "mdn" }) emptyMdnQuery() { return "Type 'mdn <search-term>' to search MDN web docs"; }
@FlowPlugin.Search(startsWith: "mdn ") searchMdnQuery(query: Query) { const search = query.search.toLowerCase();
return this.mdnData .filter(v => v.title.toLowerCase().includes(search)) .map(v => ({ title: v.title, action: this.actions.openUrl(this.mdnUrlPrefix + v.url), })); }}