Skip to content
Merged
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