-
Notifications
You must be signed in to change notification settings - Fork 0
Getting Started
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:
- Creating a new Chrome extension project with a popup window and background script.
- Setting up BXPB and defining and building your protobuf service..
- 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.
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
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.
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.
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,
},
];
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()
.
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.
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.
Then click "Load unpacked" in the top-left, and choose the build/
directory.
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
.
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.
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
BXPB has two packages to use:
-
@bxpb/protoc-plugin
- Theprotoc
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!
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!
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 forgreeter_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 forgreeter_bxdescriptors.js
. -
greeter_bxservices.js
- Service code to use when implementing a BXPB service. -
greeter_bxservices.d.ts
- TypeScript definitions forgreeter_bxservices.js
. -
greeter_grpc_pb.d.ts
- If you usedgrpc_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 theGreetRequest
andGreetResponse
objects. Generated by the built-in JavaScript support inprotoc
. -
greeter_pb.d.ts
- TypeScript definitions forgreeter_pb.js
. Generated bygrpc_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.
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
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.
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.
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.
You can compare your set up to getting-started/checkpoint-3 to verify correctness.
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.