Contributing
Building
To build Glide you must have Node v24 and 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
pnpm bootstrap:mach
pnpm bootstrap will download a copy of the Firefox source code to the engine/ directory, bundle Glide's dependencies, build the docs, apply all of our patches to the Firefox source code. pnpm bootstrap:mach will download missing system dependencies, and configure Firefox's internal mach tool.
To actually build Glide, you can run:
pnpm build
This can take quite a long time, a fresh build takes ~30 mins on an M4 Max.
pnpm bootstrap:mach configures git to use watchman for tracking file access. On many systems, this means that running git commands in the engine directory, or running many Glide tests at once, will hit the watch limit. See the watchman docs for how to avoid this.
On Linux, you can add fs.inotify.max_user_watches=524288 to /etc/sysctl.conf and then run sudo sysctl -p. On MacOS, do the same, but the parameters are kern.maxfiles and kern.maxfilesperproc.
Once you have Glide compiled, you can launch it with:
pnpm launch
There are lots of arguments you can pass here, run pnpm launch --help for the full list.
Editor setup
Pre-push hook
Glide has a pre-push hook that will run many of the lints for you automatically before each git push. To set it up, run ln -s ../../scripts/pre-push.sh .git/hooks/pre-push.
Linting
Glide uses oxlint for linting, there is a VSCode extension, Zed extension, and a oxc_language_server LSP available for oxlint.
Alternatively, you can run all lint checks with:
pnpm lint
To enable oxlint in Nvim 11, you can use the following config:
vim.lsp.config('oxc', {
cmd = {"npx", "oxc_language_server"},
root_dir = function(buf, on_dir)
local dir = vim.fs.root(0, { 'package.json', 'tsconfig.json' })
if dir then on_dir(dir) end
end,
})
Formatting
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:
- The main way to interact with the Firefox build system is through the mach CLI, accessible through pnpm mach.
- Tests are written using mochitest and can be invoked with pnpm mach test glide. More details here.
- Files are included in the Firefox build through JAR Manifests.
- All interaction with web content is centralised to a single JS Actor that we define, GlideHandlerChild.
- JS imports must use Firefox's ChromeUtils.importESModule(), types can be imported with import type { .. } from '...'.
Primer on Glide specific concepts:
- 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
- We have an internal script pnpm firefox:patch to patch the Firefox source code
- 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.
.mts reloading
Most of glide is implemented in bundled Typescript files. These files are loaded on browser start, but not on new tabs or windows. You can reload them by closing the browser and rerunning pnpm launch.
Logging
Generally, use GlideBrowser._log. Some parts of the code use a more specific logger; search for console.createInstance().
By default, only error level logging is shown. You can enable more verbose logging by passing --setpref="glide.logging.loglevel=Debug" to pnpm launch. See ConsoleLogLevel for a list of available levels.
For more information on Firefox's logging system, see the upstream Firefox docs.
User config
The config file in src/glide.ts will take precedence over the user-wide config. pnpm bootstrap creates an empty config automatically, but you can edit it manually for testing.
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 naming convention tests must follow is browser_$name.ts.
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();
You can also filter tests by directory:
pnpm mach test glide/browser/base/content/test/config/
Or individual file:
pnpm mach test glide/browser/base/content/test/config/dist/browser_include.js
The file has to be the dist/$file.js version, you cannot pass TypeScript files yet.
Tests sometimes fail with this message:
"FAIL uncaught exception - NotFoundError: Node.insertBefore:
Child to insert before is not a child of this node
at apply_mutations/<@chrome://glide/content/document-mirror.mjs:170:23"
This is a known flakiness and does not indicate an issue with your local environment.
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
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",
},
);