Skip to content

[MV3 Debug Extension] The new debug extension can be run on Manifest V3 or Manifest V2 #1966

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

Merged
merged 11 commits into from
Feb 16, 2023
26 changes: 22 additions & 4 deletions dwds/debug_extension_mv3/tool/build_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// Run from the extension root directory:
// - For dev: dart run tool/build_extension.dart
// - For prod: dart run tool/build_extension.dart prod
// - For MV3: dart run tool/build_extension.dart --mv3

import 'dart:async';
import 'dart:convert';
Expand All @@ -19,20 +20,26 @@ import 'package:args/args.dart';
import 'package:path/path.dart' as p;

const _prodFlag = 'prod';
const _mv3Flag = 'mv3';

void main(List<String> arguments) async {
final parser = ArgParser()
..addFlag(_prodFlag, negatable: true, defaultsTo: false);
..addFlag(_prodFlag, negatable: true, defaultsTo: false)
..addFlag(_mv3Flag, negatable: true, defaultsTo: false);
final argResults = parser.parse(arguments);

exitCode = await run(isProd: argResults[_prodFlag] as bool);
exitCode = await run(
isProd: argResults[_prodFlag] as bool,
isMV3: argResults[_mv3Flag] as bool,
);
if (exitCode != 0) {
_logWarning('Run terminated unexpectedly with exit code: $exitCode');
}
}

Future<int> run({required bool isProd}) async {
_logInfo('Building extension for ${isProd ? 'prod' : 'dev'}');
Future<int> run({required bool isProd, required bool isMV3}) async {
_logInfo(
'Building ${isMV3 ? 'MV3' : 'MV2'} extension for ${isProd ? 'prod' : 'dev'}');
_logInfo('Compiling extension with dart2js to /compiled directory');
final compileStep = await Process.start(
'dart',
Expand All @@ -43,6 +50,17 @@ Future<int> run({required bool isProd}) async {
if (compileExitCode != 0) {
return compileExitCode;
}
final manifestFileName = isMV3 ? 'manifest_mv3' : 'manifest_mv2';
_logInfo('Copying manifest.json to /compiled directory');
try {
File(p.join('web', '$manifestFileName.json')).copySync(
p.join('compiled', 'manifest.json'),
);
} catch (error) {
_logWarning('Copying manifest file failed: $error');
// Return non-zero exit code to indicate failure:
return 1;
}
_logInfo('Updating manifest.json in /compiled directory.');
final updateStep = await Process.start(
'dart',
Expand Down
2 changes: 1 addition & 1 deletion dwds/debug_extension_mv3/tool/update_dev_files.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Future<void> _updateManifestJson() async {
_newKeyValue(
oldLine: line,
newKey: 'name',
newValue: '[DEV] MV3 Dart Debug Extension',
newValue: '[DEV] Dart Debug Extension',
),
if (extensionKey != null)
_newKeyValue(
Expand Down
52 changes: 1 addition & 51 deletions dwds/debug_extension_mv3/web/chrome_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,16 @@ external Chrome get chrome;
@JS()
@anonymous
class Chrome {
external Action get action;
external Debugger get debugger;
external Devtools get devtools;
external Notifications get notifications;
external Runtime get runtime;
external Scripting get scripting;
external Storage get storage;
external Tabs get tabs;
external WebNavigation get webNavigation;
external Windows get windows;
}

/// chrome.action APIs
/// https://developer.chrome.com/docs/extensions/reference/action

@JS()
@anonymous
class Action {
external void setIcon(IconInfo iconInfo, Function? callback);

external OnClickedHandler get onClicked;
}

@JS()
@anonymous
class OnClickedHandler {
external void addListener(void Function(Tab tab) callback);
}

@JS()
@anonymous
class IconInfo {
external String get path;
external factory IconInfo({String path});
}

/// chrome.debugger APIs:
/// https://developer.chrome.com/docs/extensions/reference/debugger

Expand All @@ -57,7 +31,7 @@ class Debugger {
external void attach(
Debuggee target, String requiredVersion, Function? callback);

external Object detach(Debuggee target);
external void detach(Debuggee target, Function? callback);

external void sendCommand(Debuggee target, String method,
Object? commandParams, Function? callback);
Expand Down Expand Up @@ -224,30 +198,6 @@ class MessageSender {
external factory MessageSender({String? id, String? url, Tab? tab});
}

/// chrome.scripting APIs
/// https://developer.chrome.com/docs/extensions/reference/scripting

@JS()
@anonymous
class Scripting {
external Object executeScript(InjectDetails details);
}

@JS()
@anonymous
class InjectDetails<T, U> {
external Target get target;
external T? get func;
external List<U?>? get args;
external List<String>? get files;
external factory InjectDetails({
Target target,
T? func,
List<U>? args,
List<String>? files,
});
}

@JS()
@anonymous
class Target {
Expand Down
19 changes: 9 additions & 10 deletions dwds/debug_extension_mv3/web/debug_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ library debug_session;

import 'dart:async';
import 'dart:convert';
import 'dart:html';

import 'package:built_collection/built_collection.dart';
import 'package:collection/collection.dart' show IterableExtension;
Expand Down Expand Up @@ -136,15 +135,15 @@ void detachDebugger(
final debugSession = _debugSessionForTab(tabId, type: type);
if (debugSession == null) return;
final debuggee = Debuggee(tabId: debugSession.appTabId);
final detachPromise = chrome.debugger.detach(debuggee);
await promiseToFuture(detachPromise);
final error = chrome.runtime.lastError;
if (error != null) {
debugWarn(
'Error detaching tab for reason: $reason. Error: ${error.message}');
} else {
_handleDebuggerDetach(debuggee, reason);
}
chrome.debugger.detach(debuggee, allowInterop(() {
final error = chrome.runtime.lastError;
if (error != null) {
debugWarn(
'Error detaching tab for reason: $reason. Error: ${error.message}');
} else {
_handleDebuggerDetach(debuggee, reason);
}
}));
}

void _registerDebugEventListeners() {
Expand Down
1 change: 0 additions & 1 deletion dwds/debug_extension_mv3/web/devtools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ void _registerListeners() {
Object _,
String storageArea,
) {
if (storageArea != 'session') return;
_maybeCreatePanels();
}));
}
Expand Down
4 changes: 4 additions & 0 deletions dwds/debug_extension_mv3/web/lifeline_ports.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Port? _lifelinePort;
int? _lifelineTab;

Future<void> maybeCreateLifelinePort(int tabId) async {
// This is only necessary for Manifest V3 extensions:
if (!isMV3) return;
// Don't create a lifeline port if we already have one (meaning another Dart
// app is currently being debugged):
if (_lifelinePort != null) {
Expand All @@ -36,6 +38,8 @@ Future<void> maybeCreateLifelinePort(int tabId) async {
}

void maybeRemoveLifelinePort(int removedTabId) {
// This is only necessary for Manifest V3 extensions:
if (!isMV3) return;
// If the removed Dart tab hosted the lifeline port connection, see if there
// are any other Dart tabs to connect to. Otherwise disconnect the port.
if (_lifelineTab == removedTabId) {
Expand Down
32 changes: 32 additions & 0 deletions dwds/debug_extension_mv3/web/manifest_mv2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "Dart Debug Extension",
"version": "1.31",
"manifest_version": 2,
"devtools_page": "static_assets/devtools.html",
"browser_action": {
"default_icon": "static_assets/dart_dev.png"
},
"externally_connectable": {
"ids": ["nbkbficgbembimioedhceniahniffgpl"]
},
"permissions": [
"debugger",
"notifications",
"storage",
"tabs",
"webNavigation"
],
"host_permissions": ["<all_urls>"],
"background": {
"scripts": ["background.dart.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["detector.dart.js"],
"run_at": "document_end"
}
],
"web_accessible_resources": ["debug_info.dart.js"],
"options_page": "static_assets/settings.html"
}
3 changes: 0 additions & 3 deletions dwds/debug_extension_mv3/web/panel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,6 @@ void _handleRuntimeMessages(
}

void _handleStorageChanges(Object storageObj, String storageArea) {
// We only care about session storage objects:
if (storageArea != 'session') return;

interceptStorageChange<DebugInfo>(
storageObj: storageObj,
expectedType: StorageObject.debugInfo,
Expand Down
4 changes: 4 additions & 0 deletions dwds/debug_extension_mv3/web/storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:js/js.dart';
import 'chrome_api.dart';
import 'data_serializers.dart';
import 'logger.dart';
import 'utils.dart';

enum StorageObject {
debugInfo,
Expand Down Expand Up @@ -131,6 +132,9 @@ void interceptStorageChange<T>({
}

StorageArea _getStorageArea(Persistance persistance) {
// MV2 extensions don't have access to session storage:
if (!isMV3) return chrome.storage.local;

switch (persistance) {
case Persistance.acrossSessions:
return chrome.storage.local;
Expand Down
75 changes: 68 additions & 7 deletions dwds/debug_extension_mv3/web/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'dart:js_util';
import 'package:js/js.dart';

import 'chrome_api.dart';
import 'logger.dart';

Future<Tab> createTab(String url, {bool inNewWindow = false}) {
final completer = Completer<Tab>();
Expand Down Expand Up @@ -69,19 +70,32 @@ Future<bool> removeTab(int tabId) {
}

Future<bool> injectScript(String scriptName, {required int tabId}) async {
await promiseToFuture(chrome.scripting.executeScript(InjectDetails(
target: Target(tabId: tabId),
files: [scriptName],
)));
return true;
if (isMV3) {
await promiseToFuture(_executeScriptMV3(_InjectDetails(
target: Target(tabId: tabId),
files: [scriptName],
)));
return true;
} else {
debugWarn('Script injection is only supported in Manifest V3.');
return false;
}
}

void onExtensionIconClicked(void Function(Tab) callback) {
chrome.action.onClicked.addListener(callback);
if (isMV3) {
_onExtensionIconClickedMV3(callback);
} else {
_onExtensionIconClickedMV2(callback);
}
}

void setExtensionIcon(IconInfo info) {
chrome.action.setIcon(info, /*callback*/ null);
if (isMV3) {
_setExtensionIconMV3(info, /*callback*/ null);
} else {
_setExtensionIconMV2(info, /*callback*/ null);
}
}

bool? _isDevMode;
Expand All @@ -97,6 +111,20 @@ bool get isDevMode {
return isDevMode;
}

bool? _isMV3;

bool get isMV3 {
if (_isMV3 != null) {
return _isMV3!;
}
final extensionManifest = chrome.runtime.getManifest();
final manifestVersion =
getProperty(extensionManifest, 'manifest_version') ?? 2;
final isMV3 = manifestVersion == 3;
_isMV3 = isMV3;
return isMV3;
}

String addQueryParameters(
String uri, {
required Map<String, String> queryParameters,
Expand All @@ -108,3 +136,36 @@ String addQueryParameters(
});
return newUri.toString();
}

@JS('chrome.browserAction.onClicked.addListener')
external void _onExtensionIconClickedMV2(void Function(Tab tab) callback);

@JS('chrome.action.onClicked.addListener')
external void _onExtensionIconClickedMV3(void Function(Tab tab) callback);

@JS('chrome.browserAction.setIcon')
external void _setExtensionIconMV2(IconInfo iconInfo, Function? callback);

@JS('chrome.action.setIcon')
external void _setExtensionIconMV3(IconInfo iconInfo, Function? callback);

@JS()
@anonymous
class IconInfo {
external String get path;
external factory IconInfo({required String path});
}

@JS('chrome.scripting.executeScript')
external Object _executeScriptMV3(_InjectDetails details);

@JS()
@anonymous
class _InjectDetails {
external Target get target;
external List<String>? get files;
external factory _InjectDetails({
Target target,
List<String>? files,
});
}
Loading