-
Notifications
You must be signed in to change notification settings - Fork 1
Technical documentation
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.).
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.
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:
- 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 therequire
script to fail, since the modules haven't been defined. - DesModder's injected preload script sets up its own module overrides through the
ALMOND_OVERRIDES
proxy. If it detects thatALMOND_OVERRIDES
is already defined, it will overwrite it and alert the user about compatibility issues. - Once
ALMOND_OVERRIDES
is defined, DesModder injects thecalculator_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. - After the
calculator_desktop-[hash].js?
finishes executing, DesModder finally runs therequire
script to initialize the calculator.
Desmos Unlocked must be aware of this process in order to function alongside DesModder.
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 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 tofetch
the URLhttps://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.
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.
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).