Glide is in very early alpha. There will be many missing features and bugs.

Contributing

To build Glide you must have pnpm installed.

You must also verify that your system has the dependencies that Firefox requires:

After cloning the repository

git clone https://github.com/glide-browser/glide

you can set up your local repo with:

pnpm install
pnpm bootstrap

This will download a copy of the Firefox source code to the engine/ directory, bundle Glide's dependencies, build the docs, and apply all of our patches to the Firefox source code.

To actually build Glide, you can run:

pnpm build
important

This can take quite a long time, a fresh build takes ~30 mins on an M4 Max.

Once you have Glide compiled, you can launch it with:

pnpm launch
tip

There are lots of arguments you can pass here, run pnpm launch --help for the full list.

Editor setup

Glide uses dprint for formatting, if you have not used dprint before, it is recommended you install and configure it.

Alternatively, you can run auto-formatting with:

pnpm fmt

Getting acclimated

Glide inherits a lot of concepts from Firefox, for a quick primer:

  1. The main way to interact with the Firefox build system is through the mach CLI, accessible through pnpm mach.
  2. Tests are written using mochitest and can be invoked with pnpm mach test glide. More details here.
  3. Files are included in the Firefox build through JAR Manifests.
  4. All interaction with web content is centralised to a single JS Actor that we define, GlideHandlerChild.
  5. JS imports must use Firefox's ChromeUtils.importESModule(), types can be imported with import type { .. } from '...'.

Primer on Glide specific concepts:

  1. TypeScript files are converted to JS by stripping all TS syntax with ts-blank-space and stored in a relative ./dist/ directory.
  • Note this means you cannot use any TS syntax that has a runtime effect, e.g. enum
  1. We have an internal script pnpm firefox:patch to patch the Firefox source code
  2. Development practically requires running an FS watcher, see below.

Working on Glide

You should always have a terminal open running our file watcher:

pnpm dev

This handles:

  • Compiling TS files to JS
  • Rebuilding docs
  • Copying source files to the Firefox engine/ directory

If you have the watcher running, you should hardly ever have to explicitly rebuild.

Tests

Tests are written using mochitest and located in src/glide/browser/base/content/test/.

You can run all Glide's tests with:

pnpm test
# or
pnpm mach test glide

By default, tests run in a full browser window, however this means that you cannot do anything else while the tests are running. Instead, you can run tests in the background with:

pnpm test --headless

When adding a new test file, you must include it in the browser.toml file for that test directory, for example:

[DEFAULT]
support-files = []

["dist/browser_my_test.js"]

You must point to the dist/*.js file instead of the .ts file as Firefox's test runner does not yet support directly running TS files.

The typical naming convention is browser_$name.ts but you can choose to use a different name.

A typical test file looks like this:

"use strict";

add_task(async function test_my_test_name() {
  // test
});

If you're familiar with Jest, or anything like it, mochitest works a little differently as there is no expect() API, instead the following assertion functions are provided:

function is(a: unknown, b: unknown, name?: string): void;
function isnot(a: unknown, b: unknown, name?: string): void;

// expect the comparison to fail
function todo_is(
  a: unknown,
  b: unknown,
  name?: string,
): void;

// Like `is()` but compares by stringifying to JSON first.
function isjson(
  a: unknown,
  b: unknown,
  name?: string,
): void;

// Like `is()` but expects a truthy value
function ok(a: unknown, name?: string): asserts a;
function notok(a: unknown, name?: string): void;

You can filter the test functions that are ran in a single file with .only() or .skip()

add_task(...).skip();
add_task(...).only();

TODO: mention docs type checking

Unfortunately, you cannot yet tell mochitest to run only a specific test file, but you can filter by directory, e.g.

pnpm mach test glide/browser/base/content/test/config/

Docs

The docs pages are written in Markdown and located in the src/glide/docs/ directory. The markdown is then converted to HTML using a custom Markdoc integration in src/glide/browser/base/content/docs.mts.

Syntax highlighting is performed by Shiki with a custom version of the Tokyo Night theme.

Glide also ships with a builtin file watcher to automatically reload the rendered docs if they're opened with a file:// URI, you can enable it with:

glide.prefs.set("glide.dev.reload_files", true);

And the following pref is required for the search index to work:

glide.prefs.set(
  "security.fileuri.strict_origin_policy",
  false,
);

And then open a URI like this:

file:///path-to-glide-directory/src/glide/docs/dist/contributing.html
tip

You do not actually need to build Glide from scratch to update the docs!

You can also run just the .md -> .html build step in watch mode with pnpm dev:docs.

Debugging Hints

When debugging hints, the hint popups will disappear when you try to inspect them with the Browser Toolbox. To prevent this, you can disable popup auto-hide in the Browser Toolbox, see the Firefox docs for how to do so.

Now, hints will only be cleared once activated or when <Esc> is pressed.

Concepts

TypeScript build system

We take a quite non-standard approach to shipping TS files where an FS watcher strips all of the TS syntax and replaces it with spaces before writing it to a relative ./dist/$name.js file.

This is for a couple reasons:

  • Integrating TS directly into the Firefox build system is hard
  • A strong belief that TS should just be JS + types
  • Replacing types with spaces means we do not need sourcemaps

JS Actors

To interact with web content Glide uses a single JSWindowActor, GlideHandlerChild.sys.mts.

All of the code in GlideHandlerChild.sys.mts is ran outside of the main process and communicates with the main process by sending messages.

Messages are typed and sent through the .send_async_message(name, args?) method on either the parent actor or the child actor. Messages that the parent actor (main process) can send to the child are defined in GlideHandlerParent.sys.mts::ParentMessages, and messages that the child actor can send are defined in GlideHandlerChild.sys.mts::ChildMessages.

Typical usage from the main process looks like this:

GlideBrowser.get_focused_actor().send_async_message(
  "Glide::ReplaceChar",
  {
    character: "a",
  },
);