Skip to content

FlowCrypt Project Structure and Overview

Tom J edited this page Apr 13, 2022 · 16 revisions

These notes are extracted from an email, so they lack some context. Still, they may be useful to first-time contributors, and should probably be a required reading if you are looking to contribute.

Also see:

Entrypoint, where to start

There's no entry point. Or rather, there are 50 entry points, depending type of file you're looking at (extension pages, background page, content scripts, browser action button popup pages).

Note: no MVC or front end framework here. Each page comes as a pair of two files: a .htm and a .ts. No routers, no SPA approaches. To change a view, user clicks a link which points to a different .htm (or you can change window.location.href programmatically).

Extension pages

Each page has its own .ts file with the same name. Es an example, examine the html import tags in extension/chrome/settings/modules/help.htm. The last tag is the interesting one:

<script src="help.js" type="module"></script>

In this case help.js in the same folder (same name, same folder by convention). That's your entry point. type="module" Tells the browser to treat it as ES6 module. help.js is the name after TypeScript is transpiled. Source code for this file will end with .ts. So help.ts is your entry point for the help page.

Continuing the thread, help.ts loads a bunch of deps as es6 modules on top:

import { VERSION } from '../../../js/common/core/const.js';
import { Catch } from '../../../js/common/platform/catch.js';
import { Str } from '../../../js/common/core/common.js';
import { Xss, Ui, Env } from '../../../js/common/browser.js';
import { BrowserMsg } from '../../../js/common/extension.js';
import { Api } from '../../../js/common/api/api.js';

Note: We don't transpile ES6 modules away, instead we declare in manifest that we only support browsers that do es6 modules natively.

Each page (entrypoint?) contains a View class that looks like this:

View.run(class HelpView extends View {

  private acctEmail: string | undefined;
  // ... more props ...

Next, in constructor, we check url parameters. Notice how we never use uncheckedUrlParams in the code below this, all input parameters should be somehow checked first. The project follows a fail fast and loudly philosophy. Any deviation from expected input should throw early, so that we know where to fix the problem when it surfaces.

  constructor() {
    super();
    const uncheckedUrlParams = Url.parse(['acctEmail', 'parentTabId', 'bugReport']);
    this.acctEmail = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'acctEmail');
    // .. set remaining props ..
  }

Next, rendering. Because render is async, this is a good chance to pull up some facts from storage if needed. Our example help.ts page does not have any storage needs, but if it did, it would look something like this:

  public render = async () => {
    // load primary private key - example
    const [primaryKi] = await Store.keysGet(acctEmail, ['primary']);
    Ui.abortAndRenderErrorIfKeyinfoEmpty(primaryKi); // fail fast and loudly

    // load some other stuff from local storage
    const { addresses } = await Store.getAcct(acctEmail, ['addresses']);

    // things to on initial rendering
    $('#input_email').val(this.acctEmail).attr('disabled', 'disabled');
  }

Notice how we failed fast and loudly there with Ui.abortAndRenderErrorIfKeyinfoEmpty. We don't always have to do this, because our code would get quite cluttered if we did. But for things that come from external sources (not generated by our own currently running code), we check them and fail fast. This includes local storage/database as well as page url parameters.

Then come event handlers and the rest of page logic:

  public setHandlers = () => {
    $('.action_send_feedback').click(this.setHandler(el => this.sendFeedbackHandler(el)));
  }

  private sendFeedbackHandler = async (target: HTMLElement) => {
    // respond to a click
  }

});

Background page

Background tasks happen here. Certain browser actions are only allowed from a background page. You'll find the background page at extension/js/background_page/background_page.htm, which leads you to background_page.ts in the same folder.

Content scripts

This is trickier. If you are an experienced extension developer, your intuition would first bring you to extension/manifest.js to find this:

  "content_scripts": [
    {
      "matches": [
        "*://mail.google.com/*",
        "*://inbox.google.com/*"
      ],
      "css": [
        "/css/webmail.css"
      ],
      "js": [
        "/lib/purify.min.js",
        "/lib/jquery.min.js",
        "/lib/openpgp.js",
        "/js/content_scripts/webmail_bundle.js"
      ]
    },

The purify, jquery and openpgp are dependencies and extension/js/content_scripts/webmail_bundle.js is our bundled code for content scripts. This is done in tooling/bundle-content-scripts.ts (a Nodejs script) - custom bundling solution (which compiles TS to JS and bundles results together, without otherwise transpiling the code - we run only on modern browsers). The bundling mechanism looks like this:

buildContentScript(([] as string[]).concat(
  getFilesInDir(`${sourceDir}/common/platform`, /\.js$/, false),
  getFilesInDir(`${sourceDir}/common/core`, /\.js$/, false),
  getFilesInDir(`${sourceDir}/common/api`, /\.js$/, false),
  getFilesInDir(`${sourceDir}/common`, /\.js$/, false),
  getFilesInDir(`${sourceDir}/content_scripts/webmail`, /\.js$/),
), 'webmail_bundle.js');

By (our) convention, the last js/ts file tends to be the entry point and everything else tend to be libraries / dependencies. Here, extension/js/content_scripts/webmail/webmail.ts is your entrypoint for webmail related content scripts (stuff that runs on mail.google.com and such).

Note: There is a hidden footgun above, the files are included in the bundle alphabetically. That happens to work since the entrypoint is webmail.ts, and the letter w is almost at the end of the alphabet, so it ends up last. We don't change this code very often, so it's what it is for now, but it should be fixed.

Where to put general code

Shared code that you expect to reuse across the project would go somewhere on the extension/js/common folder, as a method of an appropriate class, or a new class.

Difference between builds

When you build the project, in build/ you will find firefox-consumer, chrome-consumer and chrome-enterprise folders.

Consumer and enterprise versions share the same code, but have slightly different manifests, and certain features may be enabled or disabled in a specific build type with a flag.

Generally speaking, unless you are working on an enterprise-specific feature, you should be using consumer versions for debugging and development. Once you commit and push the changes, our CI tests will test your changes on both types of builds.

Project dependencies

You can mostly ignore dependencies listed in package.json - they are for development only. Actual extension dependencies are checked into the repo in extension/lib.

There you'll find, among others:

  • bootstrap - not used much, mostly used in extension/chrome/settings/index.htm
  • featherlight - used as a lightbox to render pages in settings
  • fine-uploader - user file uploads (compose box)