Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Major rewrite to support manifest v3 #10

Merged
merged 8 commits into from
Jan 10, 2023
Merged

Major rewrite to support manifest v3 #10

merged 8 commits into from
Jan 10, 2023

Conversation

kawazoe
Copy link

@kawazoe kawazoe commented Dec 28, 2022

This PR is a major rewrite, refactoring most of the original code to better use modern javascript tools like modules, rollup, and the component pattern. The result is a version of AuRo that supports manifest v3, is easy to debug, and by changing how data is passed around gets to benefit from a better UX when something goes wrong. Here's a rundown of everything that changed and the reasoning behind every major decisions.

See #4

RollupJs

Modularity

Something I noticed in the original code was that it attempted to be modular, but fell short due to the complex structure and limitations that comes with Chrome extensions. Any attempt to share code is basically impossible "out-of-the-box". While the service worker, popup, and content script supports modules, the injected scripts requires everything to be available in the page directly. The initial code simply duplicated the code, but this can and did lead to subtle differences between the implementations.

This PR solves this problem by introducing Rollupjs to the project. Rollup is a simple nodejs based toolchain that transforms source code into a deployable bundle. It handles things like minification and concatenation, but mostly, through the use of plugins, also supports very complex bundling scenarios. In this case, we use the rollup-plugin-chrome-extension which handles most of the grunt work for us. It read the extension manifest.json and produces the appropriate content, popup, and service worker bundles for us.

This change, combined with a custom supplemental bundle (see rollup.config.js:24), means that we can now share code between the extension and the code injected in the page.

To produce the production bundle, run npm run build.

Manual Testing

A big pain point of working in chrome extensions is the constant reloading that have to be done in the extension management page of the browser every time you want to test a change. This can become very tedious in a project like this where every change happen one small refactoring/redesign at a time and have to be tested constantly to ensure that the original behavior is still there.

Thanks to rollup, we can instead spin-up a tiny web server that monitor file changes and communicate via web-socket with the extension to force it to reload every time a file updates. This is done for us via the simpleReloader module of the chrome extension plugin.

To produce the auto-updating bundle, run npm run watch.

Build Configurations

With the benefit of reusable code, and a configurable build system, this PR introduce a set of logging functions that automatically handles the AuRo :: prefix as well as the frame id prefix. These functions are declared in lib/logging.js. You will notice that they make use of special variables named __log_namespace__ and __log_verbose__. Those variables are placeholders that gets replaced automatically during the build process by values configured in the rollup.config.js file. Some of those are also dynamic, depending of command line arguments.

By default, the build script is not verbose, so any log under warn will be suppressed. The watch script, however is a verbose build, keeping all of the logs active.

Eventing

Centralized Events

Being a Chrome extension, the main communication point between its various components are doing to be messages. While the original extension used this extensively, there wasn't any centralized way of managing events. There were custom reply mechanisms in place, different event sources, and inconsistent event listener structures.

Making use of the now modular design of the project, this PR adds a centralized event "hub" in lib/events.js. Events are declared globally with makeEvent, a small function that returns a callable message emitter, with an additional on property that can be used to listen to the event with a callback.

Chrome messages supports replies out of the box, so there is no need to use custom reply events to deal with this scenario. You can simply use the return value of the emitter instead.

Chrome messages also supports async events, but they have some quirks and require that you inform the runtime that your event might reply in an async way. The event emitter automatically handles this scenario.

Chrome messages have to be dispatched at the correct "location". Events can be sent to the background runtime, or the content script of a specific tab, including specific frames, or the popup. This event library bind those different targets when the event is declared, removing this concern from the logic of the application.

Storage

Local vs Session

Storage was previously handled in a very risky way that introduced race conditions. This PR fixes those by drastically simplifying how storage is accessed and by using different storages for different needs.

Since manifest v3 makes uses a service worker, the background script will not run for the entire time of the browser session. It can be shutdown and restarted as needed. Instead of clearing tab keys when the script starts, with a risk of race condition as the operation was not awaited, the extension now stores those keys in session storage. This preserves them across service worker restarts, and ensure they are cleared before the extension load without any risk of race conditions.

Repository Pattern

With this new need for session storage for tabs, and local storage for hosts, this PR introduce a repository pattern that exposes only what is needed to manipulate storage.

Promisification

The original code spends a lot of effort converting the various Chrome callback APIs into Promises, which is a good thing. This is however supported out of the box in all versions of Chrome that also supports the Audio API the extension is using. This PR now uses the native implementation when possible.

Injected Content

Sandbox & Scripting API

The original extension depended on having full control in the content script to inject its content. This script is now sandboxed in manifest v3, requiring a different approached based on the scripting API. This API is only available from the service worker, meaning that a large portion of the code had to be restructured to move this process out of the content script.

API Change

The first prototype used window events to attempt to communicate between the extension and the injected code. This PR uses the original solution: script injection.

Popup

Components

With the many changes brought by the new modular structure, and new initialization process, we now have the opportunity of handling errors in a more user friendly way than before. The original design for the popup did not made this simple and was instead replaced by a custom, react-inspired, component architecture. It is very simple, and far from optimized, but it should be easy to replace with react if the needs ever requires something more substantial.

The core of this custom library is in popup/renderer.js and offers three methods:

  • createElement which works like its react equivalent.
  • render which takes a component (a function that returns elements), and applies it to the DOM.
  • requestRender which triggers an update when the state of the components changes.

Components are declared in popup/components in the form of a setup function that returns a render function.

New Feature - Error Handling

With this new structure, the extension can now display different error messages to handle multiple scenarios, like distinguishing between a missing authorization, or a missing content script. This also comes with a simplified and more accessible look.

New Feature - Dark Theme

Just a little thing that I included to protect my eyes and complement your already existing white icon 😃


That's it! I hope you appreciate the work that I did here as much as I appreciated your efforts in producing the initial version.

Firsts, this commit refactors the codebase to use rollup as a build tool. This switch enables multiples scenarios:

 - Better development experience with auto reload.
 - Support for es modules in the extension.
 - Support for environment variables.

This change means that the project now requires npm (node.js).
This change also means that the extension name, description, and version are now managed via the package.json.

Second, this commit makes heavy use of the new tooling by extracting common code into a small library that handles: some dom work, logging, storage, and eventing. The various entry points can then make use of this library and focus on actual logic instead of low level tasks.
It used to be that, if a frame used the HTTP-Feature-Policy header to block the permissions we need to call getUserMedia, the extension would fail with a permission error, even though the user got prompted for permissions.
Adds instructions to build the extension and automate the process of enabling verbose logging for dev builds.
Uses a component system similar to react with an ultra-lightweight custom renderer.
@ish- ish- merged commit 1dd4b6a into ish-:manifest-v3 Jan 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants