chrome_extension.dart

pub package Build Status

A library for accessing the chrome.* APIs available in Chrome extensions.

This allows to build Chrome extension with Dart & Flutter and to interop with the native APIs easily with a high-level type-safe interface.

The JS interop is build on top of dart:js_interop (static interop) which make it ready for future WASM compilation.

Buy Me A Coffee

Using the library

Example

chrome.tabs

import 'package:chrome_extension/tabs.dart';

void main() async {
  var tabs = await chrome.tabs.query(QueryInfo(
    active: true,
    currentWindow: true,
  ));
  print(tabs.first.title);
}

chrome.alarms

import 'package:chrome_extension/alarms.dart';

void main() async {
  await chrome.alarms.create('MyAlarm', AlarmCreateInfo(delayInMinutes: 2));

  var alarm = await chrome.alarms.get('MyAlarm');
  print(alarm!.name);
}

chrome.power

import 'package:chrome_extension/power.dart';

void main() {
  chrome.power.requestKeepAwake(Level.display);
}

chrome.runtime

import 'dart:js_interop';
import 'package:chrome_extension/runtime.dart';

void main() async {
  chrome.runtime.onInstalled.listen((e) {
    print('OnInstalled: ${e.reason}');
  });

  chrome.runtime.onMessage.listen((e) {
    e.sendResponse.callAsFunction(null, {'the_response': 1}.jsify());
  });
}

chrome.storage

import 'package:chrome_extension/storage.dart';

void main() async {
  await chrome.storage.local.set({'mykey': 'value'});
  var values = await chrome.storage.local.get(null /* all */);
  print(values['mykey']);
}

Available APIs

Documentation

Tips to build Chrome extensions with Flutter

Here are some personal tips to build Chrome extension using the Flutter UI framework.

Develop the app using Flutter Desktop

In order to develop in a comfortable environment with hot-reload, most of the app (the UI part) should be developed using Flutter desktop.

This requires an abstraction layer between the UI and the chrome_extension APIs.

In the Desktop entry point, a fake implementation of this abstraction layer is used, like this:

// lib/main_desktop.dart
void main() {
  // Inject a fake service that doesn't use the real chrome_extension package.
  var service = FakeBookmarkService();
  runApp(MyExtensionPopup(service));
}

abstract class BookmarkService {
  Future<List<Bookmark>> getBookmarks();
}

class FakeBookmarkService implements BookmarkService {
  @override
  Future<List<Bookmark>> getBookmarks() async => [Bookmark()];
}

Launch this entry point in desktop with
flutter run -t lib/main_desktop.dart -d macos|windows|linux

And the real entry point (the one used in the actual compiled extension) looks like:

// lib/main.dart
void main() {
  var service = ChromeBookmarkService();
  runApp(MyExtensionPopup(service));
}

class ChromeBookmarkService implements BookmarkService {
  @override
  Future<List<Bookmark>> getBookmarks() async {
    // Real implementation using chrome.bookmarks
    return (await chrome.bookmarks.getTree()).map(Bookmark.new).toList();
  }
}

Build script

web/manifest.json

{
  "manifest_version": 3,
  "name": "my_extension",
  "permissions": [
    "activeTab"
  ],
  "options_page": "options.html",
  "background": {
    "service_worker": "background.dart.js"
  },
  "action": {
    "default_popup": "index.html",
    "default_icon": {
      "16": "icons-16.png"
    }
  },
  "content_security_policy": {
    "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
  }
}
// tool/build.dart
void main() async {
  await _process.runProcess([
    'flutter',
    'build',
    'web',
    '-t',
    'web/popup.dart',
    '--csp',
    '--web-renderer=canvaskit',
    '--no-web-resources-cdn',
  ]);
  for (var script in [
    'background.dart',
    'content_script.dart',
    'options.dart'
  ]) {
    await _process.runProcess([
      Platform.resolvedExecutable,
      'compile',
      'js',
      'web/$script',
      '--output',
      'build/web/$script.js',
    ]);
  }
}

It builds the flutter app and compiles all the other Dart scripts (for example: options.dart.js, popup.dart.js, background.dart.js)

Testing

Write tests for the extension using puppeteer-dart.

import 'package:collection/collection.dart';
import 'package:puppeteer/puppeteer.dart';

void main() async {
  // Compile the extension
  var extensionPath = '...';

  var browser = await puppeteer.launch(
    headless: false,
    args: [
      '--disable-extensions-except=$extensionPath',
      '--load-extension=$extensionPath',
      // Allow to connect to puppeteer from inside your extension if needed for the testing
      '--remote-allow-origins=*',
    ],
  );

  // Find the background page target
  var targetName = 'service_worker';
  var backgroundPageTarget =
      browser.targets.firstWhereOrNull((t) => t.type == targetName);
  backgroundPageTarget ??=
      await browser.waitForTarget((target) => target.type == targetName);
  var worker = (await backgroundPageTarget.worker)!;

  var url = Uri.parse(worker.url!);
  assert(url.scheme == 'chrome-extension');
  var extensionId = url.host;

  // Go to the popup page
  await (await browser.pages)
      .first
      .goto('chrome-extension://$extensionId/popup.html');

  // Etc...
}