Compile outputs fun

Lerna to manage multiple modules - Site killer extension for Chrome and Firefox - Part 2

Published 2 years agoTypescript

This is continued from Lerna to manage multiple modules - Site killer extension for Chrome and Firefox - Part 1 .

3. Chrome browser extension

3.1 Setup

Create the directory ./packages/chrome and create package.json in it with these contents:

{
  "name": "site-killer-chrome",
  "scripts": {
    "build": "webpack",
    "dev": "webpack --watch --devtool inline-source-map"
  }
}

Next we will install the necessary dependencies for it by running this command:

npm i -D webpack webpack-cli ts-loader typescript copy-webpack-plugin zip-webpack-plugin @types/chrome
npm i ..\common

We have installed the common module as the dependency here. We also installed these additional dependencies:

Dependency Name Description
zip-webpack-plugin Make a zip file from dist directory so we can publish it to the Chrome Web Store
@types/chrome Type definition file for Chrome extension development

Then we create webpack.config.js to tell Webpack how to compile our module:

const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
const ZipPlugin = require('zip-webpack-plugin');

module.exports = {
    entry: {
        background: './src/background.ts',    // make an output for background task
        popup: './src/popup.ts'    // make an output for popup page
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            }
        ]
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.js']
    },
    plugins: [
        new CopyPlugin({    // copy common assets to dist directory
            patterns: [
                { from: './node_modules/site-killer-common/assets' }
            ]
        }),
        new ZipPlugin({    // zip the dist directory for distribution later
            path: path.resolve(__dirname, 'out'),
            filename: 'site-killer.zip'
        })
    ],
    watchOptions: {    // ignore output directories so we don't trigger the Webpack infinitely here
        ignored: /\/chrome\/(dist|out)\//
    }
};

Next we create the tsconfig.json to tell the Typescript compiler how to compile our module:

{
    "compilerOptions": {
        "sourceMap": true
    }
}

This is similar to how we setup the common module, except that we don't need the declaration file here because we are not making a library.

3.2 Code the logics

We will create ./src/background.ts file to record the visited list and kill the tab if the URL is in the blocked list. Put these contents in it:

import { blockedList, visitedList } from 'site-killer-common';

chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
    const url = new URL(tab.url);
    if (blockedList.contains(url.hostname)) {
        await new Promise(resolve =>
            chrome.tabs.remove(tabId, resolve));

        if (!chrome.runtime.lastError) {
            await new Promise(resolve =>
                chrome.notifications.create({
                    message: `Killed ${url}`,
                    title: 'Site Killer',
                    iconUrl: 'icon.png',
                    type: 'basic'
                }, resolve));
        }
    } else {
        visitedList.add(url);
    }
});

It should be self explained here. Note that we will use Promise instead of callback because it makes the code much more readable if we used it with async / await keyword.

Now we will create ./src/popup.ts that will show the popup page. Put these contents in it:

import { blockedList, globalEvents, popupComponent } from 'site-killer-common';

globalEvents.rerun.register(async () => {
    const tabs = await new Promise<chrome.tabs.Tab[]>(resolve =>
        chrome.tabs.query({}, resolve));

    let killed = 0;
    for (const tab of tabs) {
        const url = new URL(tab.url);
        if (blockedList.contains(url.hostname)) {
            await new Promise(resolve =>
                chrome.tabs.remove(tab.id, resolve));
            killed++;
        }
    }

    await new Promise(resolve =>
        chrome.notifications.create({
            message: `Killed ${killed} tabs`,
            title: 'Site Killer',
            iconUrl: 'icon.png',
            type: 'basic'
        }, resolve));
});

document.body.appendChild(popupComponent());

It will render the popup component and also register for the rerun event so that it can apply the rules to currently opened tabs. The rerun logic has to be put here instead of the common module because it relies on Chrome specific functionality.

3.3 Manifest file

We need a manifest file to make Chrome recognize it as a browser extension. The manifest file can be shared between Chrome and Firefox, so we will do it in the common module.

Goto your common module directory and create ./assets/manifest.json :

{
    "manifest_version": 2,
    "name": "Site Killer",
    "version": "2.1",
    "description": "Kill unwanted sites",
    "icons": {
        "128": "icon.png"    // go find an icon from somewhere
    },
    "background": {
        "scripts": [
            "background.js"    // the browser will run this script when your extension is loaded
        ],
        "persistent": false
    },
    "browser_action": {
        "default_icon": {
            "128": "icon.png"    // the browser will show this icon as a button beside the address bar
        },
        "default_popup": "popup.html"    // the browser will show this page when the icon is clicked
    },
    "permissions": [
        "notifications",    // the extension will show a notification when it killed a tab
        "tabs"    // the extension will need to know the tab URL
    ]
}

You will need to find an icon somewhere and put it in the ./assets directory. Next we will create ./assets/popup.html :

<html>

<head></head>

<body style="min-width: 200px">
    <script src="popup.js"></script>
</body>

</html>

The popup HTML file is simple, it just load the popup.js . The code will craft the interface.

3.4 Test

Now you can run this command in the root directory to build it:

npm run build

To test it in the Chrome browser, you will need to go to the browser extension page first.

Then you enable the Developer mode.

Then you click on the Load unpacked button to load your extension. Locate the ./dist directory in the Chrome module.

Then you should see your extension loaded in the page.

The icon of your extension should appear in the top right corner now.

Next we will work on the real Firefox extension .