Skip to content

Technical documentation

Matthew S edited this page Jul 26, 2022 · 12 revisions

🔧 Tools

Desmos Unlocked uses Node.js to build and bundle (using Webpack) TypeScript source code into the JavaScript that is delivered to the browser. The popup UI is written in 'vanilla' TypeScript (i.e. without any frameworks like React, Vue, etc.).

         

📁 File structure

The src/ directory contains all of the project's TypeScript source. The extension's background page is built from src/background/${browser}/background.ts. The src/preload/ directory contains the code responsible for the extension's initialization process, containing the content script src/preload/content.ts which (after being built) is injected at document_start. The content script injected at document_idle is built from src/content.ts. src/utils/ and src/globals/ house useful modules to be imported by the main entry points. Consult webpack/webpack.common.js for more information about the build process.

public/ contains the various non-TypeScript files that must be copied into the distribution directory in order to be used by the extension. These include the extension manifests, the Desmos Unlocked logo, the popup HTML and style sheet, and more.

webpack/ houses the Webpack configuration files.

Various dotfiles and configuration files for TypeScript and Node are located at the top level of the project.

⏳️ Preload

Module overrides

Desmos Unlocked delivers the same features to all users, no matter the browser. One major feature is the extension of the MathQuill source code used by the graphing calculator. By default, Desmos uses an extremely limited version of the MathQuill formula editor, which supports symbols like θ (theta) and γ (gamma), but not (nabla) or (hbar). In order to provide these symbols, Desmos Unlocked overrides the mathquill_src RequireJS module definition before the calculator is initialized, if the user has enabled the extended symbols feature. This override must happen before the module code is executed, and, to further complicate things, must be compatible with DesModder, another popular browser extension for Desmos, which does its own preload modifications.

Before exploring how Desmos Unlocked overrides modules, it's important to understand how modules work in the graphing calculator. Desmos uses RequireJS to manage modules, which are JavaScript files that can be loaded from another file. A developer can define a chunk of code they would like to export and later require it to load it and its dependencies. The HTML source of www.desmos.com/calculator contains the script:

<script src="/assets/build/calculator_desktop-[hash].js"></script>

which is an essential step of loading the calculator. /assets/build/calculator_desktop-[hash].js is filled with lines like:

define('keys',['require','browser'],function(require){ /* ... */ });

which, in this case, defines a module keys, with dependencies require and browser, which will run the given function when loaded. Later in the HTML, the page contains the script:

<script>
    require(['toplevel/calculator_desktop', 'testbridge', 'jquery'], function (calcPromise, TestBridge, $) {
      calcPromise.then(function(calc) {
        $('.dcg-loading-div-container').hide();
        window.Calc = calc;
        TestBridge.ready();
      });
    });
</script>

which loads and executes the modules defined in calculator_desktop-[hash].js and, once loaded, removes the loading screen and initializes the calculator.

Desmos Unlocked is able to override module definitions through the ALMOND_OVERRIDES object. At the top of calculator_desktop-[hash].js, the lines:

/* ... */ "undefined"!=typeof ALMOND_OVERRIDES&&ALMOND_OVERRIDES&&(define=ALMOND_OVERRIDES.define||define,require=ALMOND_OVERRIDES.require||require,requirejs=ALMOND_OVERRIDES.requirejs||requirejs);
define("core/vendor/almond", function(){});

show that define can be overridden if ALMOND_OVERRIDES.define exists prior to the execution of calculator_desktop-[hash].js. The key then is to delay the execution of calculator_desktop-[hash].js until the extension can create ALMOND_OVERRIDES.define, which will behave exactly the same as the regular define, except if the module being defined is mathquill_src, in which case the definition will be replaced by the extended MathQuill source. To do this, the extension makes ALMOND_OVERRIDES a Proxy, which will intercept accesses to ALMOND_OVERRIDES.define. If DesModder is installed, ALMOND_OVERRIDES will be a Proxy for a Proxy. Consult src/preload/script.ts for more details.

As will be discussed in more detail, Desmos Unlocked's behavior changes based on whether or not DesModder is enabled, in order to maintain compatibility. The important parts of DesModder's initialization process are the following:

  1. DesModder blocks net requests to URLs of the form https://www.desmos.com/assets/build/calculator_desktop-*.js in order to stop the normal module definitions. This will cause the require script to fail, since the modules haven't been defined.
  2. DesModder's injected preload script sets up its own module overrides through the ALMOND_OVERRIDES proxy. If it detects that ALMOND_OVERRIDES is already defined, it will overwrite it and alert the user about compatibility issues.
  3. Once ALMOND_OVERRIDES is defined, DesModder injects the calculator_desktop-[hash].js script to define all the modules, except it appends a ? to the URL so that it is no longer blocked by its own net request rules. If this script fails to load for some reason, DesModder will abort its initialization and the calculator will fail to load.
  4. After the calculator_desktop-[hash].js? finishes executing, DesModder finally runs the require script to initialize the calculator.

Desmos Unlocked must be aware of this process in order to function alongside DesModder.

Cross-browser compatibility

One of the largest differences in web extension behaviors across browsers comes from the version of the manifest supported. In order to work on all major browsers, Desmos Unlocked's preload behavior differs between browsers supporting manifest v2 (Firefox) and those supporting manifest v3 (Chrome, Edge, and Opera), due to the changes made to the webRequest API. See this migration guide for more differences between manifest v2 and manifest v3.

Manifest v2 behavior

Manifest v2 supports the webRequestBlocking permission, which allows for a simple way to run preload modifications before a request for calculator_desktop-[hash].js is allowed to go through. The background script, compiled from src/background/${browser}/background.ts, listens for the onBeforeRequest event for the URLs https://www.desmos.com/assets/build/calculator_desktop-[hash].js and https://www.desmos.com/assets/build/calculator_desktop-[hash].js?. When the event is detected, the following actions are taken:

  • If the URL is https://www.desmos.com/assets/build/calculator_desktop-[hash].js, the script first asks the preload content script if DesModder is enabled. The content script tries to fetch the URL https://www.desmos.com/assets/build/calculator_desktop-this_does_not_exist.js. If the request is blocked outright, then DesModder's net request rules are enabled and so DesModder is enabled. If the request succeeds, but returns 404 not found, then DesModder is not enabled. The content script sends its response back the background script.
    • If the background script is told that DesModder is enabled, it allows the request to go through--it will be blocked by DesModder's net request rules anyway.
    • If DesModder is not enabled, then the background script tells the preload content script to do its module overrides immediately, and, once done, it allows the request to proceed and the calculator to be initialized.
  • If the URL is https://www.desmos.com/assets/build/calculator_desktop-[hash].js?, then the background script already knows that DesModder is enabled, has set up its own overrides, and is trying to run the module definitions. The background script tells the preload content script to do its module overrides immediately, and, once done, it allows the request to proceed and the calculator to be initialized.

The actions above are all done synchronously with the web request. The web request cannot proceed until the actions have finished. This method is not possible with manifest v3, due to the removal of the webRequestBlocking permission.

Manifest v3 behavior

In some ways the manifest v3 behavior is simpler, but it is also more susceptible to compatibility issues if major changes are made to DesModder's initialization behavior. Under manifest v3, Desmos Unlocked uses the declarativeNetRequestWithHostAccess API to handle net requests. Rules for handling requests to calculator_desktop are updated dynamically through the background script whenever a user enables/disables the extended shortcuts feature. When enabled, the rules try to redirect all requests to https://www.desmos.com/assets/build/calculator_desktop-[hash].js and https://www.desmos.com/assets/build/calculator_desktop-[hash].js? to the preload script. This script:

  • creates the module overrides,
  • injects the calculator_desktop-[hash].js script to define all the modules, except it appends ?? to the URL so that it is no longer blocked by the net request rules, and finally
  • runs the require script to initialize the calculator.

Note that the script does not need to check if DesModder is enabled. If DesModder is not enabled, the redirect will be detected from https://www.desmos.com/assets/build/calculator_desktop-[hash].js, and after that script is effectively blocked by the redirection, the overrides and initialization can proceed. If, however, DesModder is enabled, then the script will never detect a redirect from https://www.desmos.com/assets/build/calculator_desktop-[hash].js, since DesModder will block the request before it can be redirected. Instead, it will detect a redirect coming from https://www.desmos.com/assets/build/calculator_desktop-[hash].js?, and similarly after that script is redirected, the overrides and initialization can proceed. So no special logic is needed to check for DesModder. However, the preload script is itself responsible for initializing the calculator in the manifest v3 versions. This means it needs to do some heavy lifting that is avoided altogether in the manifest v2 version.

✔️ Post-load

After the page has loaded (at document_idle), the extension injects a content script which simply adds a listener to update the MathQuill global configuration when the user's stored preferences change. This means that configuration changes made in the popup UI are immediately reflected in the graphing calculator, without the need to reload the page (excluding the enabling/disabling of extended shortcuts).