Skip to content
This repository has been archived by the owner on Jun 21, 2020. It is now read-only.

Getting Started

Douglas Parker edited this page Jun 20, 2020 · 9 revisions

NOTE: BXPB is currently shut down, unmaintained, and not suitable for production use! See the current status for more information.

This tutorial will show you how to get started with BXPB by:

  1. Creating a new Chrome extension project with a popup window and background script.
  2. Setting up BXPB and defining and building your protobuf service..
  3. Implementing the service in the background script and calling the service from the popup window.

This tutorial assumes you have basic familiarity with NodeJS, NPM, Git, and protocol buffers. It also assumes that you already have Node and NPM installed on your system. The example uses Unix shell script, which should work for most Unix/Linux and MacOS systems. Windows users may need to tweak some of the commands. Versions are pinned in this example for consistency, later versions of dependencies may work, but have not been fully tested.

This tutorial uses TypeScript and Rollup to build and bundle the Chrome extension. Neither of these tools are strictly required, but provide a straightforward way of getting to simple Chrome extension with the ideal toolchain for BXPB.

This tutorial is performed in the getting-started branch and has checkpoints for the relevant steps. You can follow along or skip forward/backward using tags in that branch (for example git checkout -f getting-started/checkpoint-1 to reset to checkpoint 1). In this branch, the root directory for the example Chrome extension is in examples/getting-started/.

If you already have a Chrome extension and you want to use BXPB in it, you can probably jump to step 2 and just work in your real project.

Create a Chrome extension

This will start off by guiding you through creating a new Chrome extension, with a simple popup window and background script.

Start by making a new directory and bootstrapping an NPM package with:

npm init -y

Initialize TypeScript

We will start by setting up TypeScript. We can bootstrap this by running:

npm install [email protected] @types/[email protected] --save-dev
node_modules/.bin/tsc --init

This will generate a tsconfig.json file with most of the relevant TypeScript settings. This will allow us to compile TypeScript code to JavaScript. It also installs some types for Chrome extension APIs, which will be useful later.

Add some TypeScript

Now that TypeScript is set up, we can add some code! We'll need two files popup.ts and background.ts. The former will run in a popup window when the user clicks on the extension's icon, while latter will execute in a different context in the background.

Define a popup.ts file with the content:

console.log('I am the popup!');

Define a background.ts file with the content:

console.log('I am the background script!');

We will also need an HTML file to display for the popup. Add a popup.html file with the content:

<!DOCTYPE html>
<html>
  <head>
    <title>Popup</title>
  </head>
  <body>
    <h1>I am the popup!</h1>

    <script src="/popup.js"></script>
  </body>
</html>

Now that we have all the raw source files we need, it's time to build the extension.

Initialize Rollup

The next tool we need is a bundler. Sadly, protocol buffers do not yet support ES modules, so we must use CommonJS. Since these protobufs will be used in a Chrome extension, we need a bundler to handle require() calls (since Chrome does not know what to do with them). Rollup serves this purpose, so we start by installing the bundler and the required minimal plugins for a CommonJS TypeScript build.

npm install [email protected] @rollup/[email protected] @rollup/[email protected] @rollup/[email protected] --save-dev

Next, create a config file for Rollup at rollup.config.js. If you're not familiar with Rollup, don't worry about this part too much as it isn't really important to BXPB itself. All this does is build popup.ts and place the result in build/popup.js. It does the same to build background.ts to build/background.js. The resulting files include all their required dependencies to run correctly.

import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

// Handle warnings.
function onwarn(warning, warn) {
    // Protocol buffers use `eval()` which generates a warning in Rollup.
    // Ignore this warning as there's nothing we can do about.
    if (warning.code === 'EVAL') return;
    warn(warning);
}

export default [
    // Build JavaScript for background script.
    {
        input: 'background.ts',
        output: {
            name: 'background',
            file: 'build/background.js',
            sourcemap: 'inline',
            format: 'iife',
        },
        plugins: [
            typescript(),
            resolve({
                browser: true,
            }),
            commonjs({
                extensions: ['.js', '.ts'],
            }),
        ],
        onwarn,
    },

    // Build JavaScript for popup window.
    {
        input: 'popup.ts',
        output: {
            name: 'popup',
            file: 'build/popup.js',
            sourcemap: 'inline',
            format: 'iife',
        },
        plugins: [
            typescript(),
            resolve({
                browser: true,
            }),
            commonjs({
                extensions: ['.js', '.ts'],
            }),
        ],
        onwarn,
    },
];

Add Chrome extension manifest.

One last file is the Chrome extension manifest. This includes metadata about the extension and tells Chrome what files to load and how to load them. Add a new file manifest.json with the content:

{
  "name": "BXPB Example",
  "version": "0.0",
  "manifest_version": 2,

  "browser_action": {
      "default_popup": "popup.html"
  },

  "background": {
    "scripts": [ "background.js" ]
  },

  "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';"
}

This includes the name and version info for the extension, as well as tells Chrome to display popup.html when the user clicks the extension icon, and to load background.js as a background script. It also includes a Content Security Policy for the extension. This is as secure as it can be with the very notable exception that eval() is allowed. This is necessary for protocol buffers that we will add later, as they require the use of eval().

Executing a build

Now that we have all the pieces together, we just need to stitch them together into a complete build. Add the following build script to package.json:

{
  // Other data...
  "scripts": {
    "build": "rollup --config && cp manifest.json popup.html build/",
    // Other scripts...
  },
  // Other data...
}

Now you can build the extension by simply running:

npm run build

This will output the compiled extension to the build/ directory. This folder is entirely generated, so if you are using version control, it should be ignored.

Installing the extension

To actually install the extension, we need to tell Chrome to load the extension from a local directory. This requires enabling developer mode and loading an unpacked extension. Start by opening your browser to chrome://extensions and enabling "Developer mode" by flipping the switch in the top-right.

Enabling developer mode.

Then click "Load unpacked" in the top-left, and choose the build/ directory.

Loading an unpacked extension.

The extension is installed! You can click the B icon in the top right to view the popup page. You can inspect element with Ctrl+Shift+I (or Cmd+Shift+I on Mac) to view the console. The background page is also loaded, and you can inspect the view there to see the console for the background script.

This is all one-time set up for the extension. Going forward, any changes to source code only require npm run build and clicking the "reload" button on chrome://extensions.

Showing the various buttons of an installed Chrome extension.

Now we have a basic Chrome extension which can be built locally and installed! So far, all this configuration is fairly unrelated to BXPB itself, but necessary to have a usable extension to add it to.

You can compare your set up to getting-started/checkpoint-1 to verify correctness.

Set up BXPB

You should now be at getting-started/checkpoint-1. If you want to skip the previous steps, you can use:

git checkout -f getting-started/checkpoint-1

Install Dependencies

BXPB has two packages to use:

  • @bxpb/protoc-plugin - The protoc compiler plugin which converts .proto files to BXPB clients and services.
  • @bxpb/runtime - A common runtime used by generated code.

We'll need both of these packages, but @bxpb/protoc-plugin is only needed at build-time, so that can just be dev dependency.

npm install @bxpb/[email protected] --save-dev
npm install @bxpb/[email protected] --save

As of 1.0.0, @bxpb/runtime is only used as an implementation detail of the generated code, so we won't be using it directly. However it does need to be installed for generated code to use.

You'll also need an instance of protoc, the protocol buffer compiler that actually compiles .proto files. @bxpb/protoc-plugin is merely a plugin to this compiler. If you are already using protocol buffers and have an existing process for installing and calling protoc in your project, you can likely use that. If not, the easiest way to get started for a new project is to use the one in grpc-tools.

# If you don't already have `protoc` installed...
npm install [email protected] --save-dev

This provides the grpc_tools_node_protoc binary, which is just protoc with an extra plugin baked in (that we don't need but there's no good way to get rid of it).

protoc has built-in functionality for generating JavaScript, however it does not have direct support for TypeScript. Instead, we'll need another plugin to generate .d.ts files for protocol buffers. In this example, we'll use grpc_tools_node_protoc_ts to accomplish this.

npm install [email protected] --save-dev

Now that all the dependencies are installed, let's make some protos!

The Service Definition

We need to define a new .proto file with the service we want to call. Like all developers, we just want to say "Hello" to anyone who asks so we'll create a greeter.proto file which defines a Greeter service, with a single RPC method: Greet. This accepts a request which includes a name, and responds with a message that contains "Hello, ${name}!".

The language guide provides more information about the proto syntax and other features, which are outside the scope of this tutorial.

// greeter.proto

syntax = "proto3";

message GreetRequest {
    string name = 1;  // Name to greet.
}

message GreetResponse {
    string message = 1; // "Hello, ${name}!"
}

service Greeter {
    rpc Greet(GreetRequest) returns (GreetResponse) {}
}

Now we've got a proto file, time to build it!

Building Protos with BXPB

The most straightforward way to build the proto is with the command:

node_modules/.bin/grpc_tools_node_protoc \
    --js_out=import_style=commonjs,binary:proto/ \
    --plugin=protoc-gen-ts=node_modules/.bin/protoc-gen-ts --ts_out=proto/ \
    --plugin=protoc-gen-bxpb=node_modules/.bin/bxpb-protoc-plugin --bxpb_out=proto/ \
    greeter.proto

This will compile greeter.proto into all the JavaScript and TypeScript needed for BXPB and output it the proto/ directory. There's a lot in there, so let's break down the components:

node_modules/.bin/grpc_tools_node_protoc

This just invokes the compiler installed from grpc-tools. If you already have an install of protoc, then you can use that here instead.

--js_out=import_style=commonjs,binary:proto/

This tells the compiler to build JavaScript. It tells it to use CommonJS (as opposed to Closure modules) and to output the corresponding JavaScript to the proto/ directory.

--plugin=protoc-gen-ts=node_modules/.bin/protoc-gen-ts --ts_out=proto/

This includes the plugin from grpc_tools_node_protoc_ts which generates TypeScript definitions for the generated JavaScript code. It also outputs to the proto/ directory.

--plugin=protoc-gen-bxpb=node_modules/.bin/bxpb-protoc-plugin --bxpb_out=proto/

This includes the plugin from @bxpb/protoc-plugin which generates JavaScript and TypeScript needed specifically for BXPB. It also outputs to the proto/ directory.

greeter.proto

This simply provides the list of files to compile. In this case, we're only compiling the one file, but you could easily expand this to my_super_cool_proto_files/*.proto.

After running this command, nothing should actually print. But instead you should notice a number of files present in the proto/ subdirectory. All of these files have the greeter_ prefix from the greeter.proto file name they originated from.

  • greeter_bxclients.js - Client code to use when calling a BXPB service.
  • greeter_bxclients.d.ts - TypeScript definitions for greeter_bxclients.js.
  • greeter_bxdescriptors.js - Metadata for the compiled proto service. This is mostly an implementation detail of BXPB, and should never be used directly.
  • greeter_bxdescriptors.d.ts - TypeScript definitions for greeter_bxdescriptors.js.
  • greeter_bxservices.js - Service code to use when implementing a BXPB service.
  • greeter_bxservices.d.ts - TypeScript definitions for greeter_bxservices.js.
  • greeter_grpc_pb.d.ts - If you used grpc_tools_node_protoc, this file will also be present as that includes a gRPC-specific plugin. This file isn't actually used by BXPB and isn't needed.
  • greeter_pb.js - The core JavaScript of the GreetRequest and GreetResponse objects. Generated by the built-in JavaScript support in protoc.
  • greeter_pb.d.ts - TypeScript definitions for greeter_pb.js. Generated by grpc_tools_node_protoc_ts.

Since all the files in the proto/ directory are generated, you should also ignore this directory in your version control system.

We can integrate proto compilation into the build pipeline by simply updating NPM scripts in your package.json with this new command:

// package.json

{
  // ...
  "scripts": {
    "build": "npm run -s build-proto && npm run -s build-extension",
    "build-proto": "rm -rf proto/ && mkdir -p proto/ && grpc_tools_node_protoc --js_out=import_style=commonjs,binary:proto/ --plugin=protoc-gen-ts=node_modules/.bin/protoc-gen-ts --ts_out=proto/ --plugin=protoc-gen-bxpb=node_modules/.bin/bxpb-protoc-plugin --bxpb_out=proto/ greeter.proto",
    "build-extension": "rollup --config && cp manifest.json popup.html build/"
  },
  // ...
}

Now simply calling npm run build will compile the protobuf and then run the normal TypeScript build with Rollup. Next step is to actually implement the service.

You can compare your set up to getting-started/checkpoint-2 to verify correctness.

Implementing and Calling the Service

You should now be at getting-started/checkpoint-2. If you want to skip the previous steps, you can use:

git checkout -f getting-started/checkpoint-2

Implementing the Service

We want to run Greeter in the background script and allow clients to send requests to it. Update background.ts to include:

// background.ts

import { serveGreeter } from './proto/greeter_bxservices';
import { GreetRequest, GreetResponse } from './proto/greeter_pb';

console.log('I am the background script!');

// Run `Greeter`, listening for requests using the `onMessage` event.
serveGreeter(chrome.runtime.onMessage, {
    // Handle a client request for the `Greet()` method.
    async Greet(req: GreetRequest): Promise<GreetResponse> {
        console.log(`Received request for name: "${req.getName()}"`);

        const res = new GreetResponse();
        res.setMessage(`Hello, ${req.getName()}!`);
        return res;
    },
});

This provides an implementation of all the Greeter RPCs. In this case, only Greet() needs to be implemented. It listens to chrome.runtime.onMessage for requests from clients. When a message is received, it is decoded to a request and the desired service and method are identified. BXPB will then call the appropriate function on user code, serializing/deserializing the protocol buffers for you. Note that we are importing serveGreeter() from the generated proto/greeter_bxservices files. This is all that is required in the background script.

Calling the Service

Now that the service is implemented, we just need to call it from the popup!

In popup.ts, add the following:

// popup.ts

import { GreeterClient } from './proto/greeter_bxclients';
import { GreetRequest } from './proto/greeter_pb';

console.log('I am the popup!');

// An immediately-invoked function expression (IIFE) to get an asynchronous context to use `await`.
(async () => {
    // Create a client which sends a message that will trigger `chrome.runtime.onMessage`.
    const client = new GreeterClient(chrome.runtime.sendMessage);

    const req = new GreetRequest();
    req.setName('Dave');
    
    // Send the RPC and get the response.
    const res = await client.Greet(req);

    console.log(`Message from background script: "${res.getMessage()}"`);
})();

This script imports GreeterClient from the generated proto/greeter_bxclients files and creates an IIFE to get an asynchronous context. It then creates the GreeterClient using chrome.runtime.sendMessage to send requests to the service. We then build a request by providing a name, calling the Greet() RPC, and await-ing the response. The message passing APIs are asynchronous, so all the RPCs of a service will return Promise objects that resolve to the RPC response type. We then read the message field of the response and print it out to the console.

The combination of chrome.runtime.sendMessage on the client and chrome.runtime.onMessage on the service are important, because they are two separate ends of the same message passing channel in Chrome extensions. There are other channels like chrome.runtime.sendMessageExternal and chrome.runtime.onMessageExternal which fulfill the same contract which are also compatible. The important part is that the two channels agree with each other. A client sending messages with chrome.runtime.sendMessage to a service listening on chrome.runtime.onMessageExternal won't be able to communicate with each other.

Checking the Result

You can test this out by running npm run build and then re-installing the Chrome extension like so. Click on the popup window and then open the inspector to see the console logs of the popup and background script.

Viewing the console logs of the final build.

You can compare your set up to getting-started/checkpoint-3 to verify correctness.

Mission Accomplished!

You should now be at getting-started/checkpoint-3. If you want to skip the previous steps, you can use:

git checkout -f getting-started/checkpoint-3

Now we have a service implemented in background.ts and being called from popup.ts. This can be expanded to include more methods which include even more functionality. Other clients can also call the service, such as content scripts or DevTools panels. Hopefully this can make cross-context communication in Chrome extensions much easier to implement and maintain.