mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
246 lines
7.5 KiB
TypeScript
246 lines
7.5 KiB
TypeScript
|
|
/**
|
||
|
|
* @license
|
||
|
|
* Copyright Google LLC All Rights Reserved.
|
||
|
|
*
|
||
|
|
* Use of this source code is governed by an MIT-style license that can be
|
||
|
|
* found in the LICENSE file at https://angular.io/license
|
||
|
|
*/
|
||
|
|
|
||
|
|
/// <reference types="chrome"/>
|
||
|
|
|
||
|
|
import {Events, Topic} from 'protocol';
|
||
|
|
|
||
|
|
export interface ContentScriptConnection {
|
||
|
|
port: chrome.runtime.Port | null;
|
||
|
|
enabled: boolean;
|
||
|
|
frameId: 'devtools' | number;
|
||
|
|
backendReady?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface DevToolsConnection {
|
||
|
|
devtools: chrome.runtime.Port | null;
|
||
|
|
contentScripts: {[name: string]: ContentScriptConnection};
|
||
|
|
}
|
||
|
|
|
||
|
|
function isNumeric(str: string): boolean {
|
||
|
|
return +str + '' === str;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface Tabs {
|
||
|
|
[tabId: string]: DevToolsConnection | undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
export class TabManager {
|
||
|
|
constructor(
|
||
|
|
private tabs: Tabs,
|
||
|
|
private runtime: typeof chrome.runtime,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
static initialize(tabs: Tabs, runtime: typeof chrome.runtime = chrome.runtime): TabManager {
|
||
|
|
const manager = new TabManager(tabs, runtime);
|
||
|
|
manager.initialize();
|
||
|
|
return manager;
|
||
|
|
}
|
||
|
|
|
||
|
|
private initialize(): void {
|
||
|
|
this.runtime.onConnect.addListener((port) => {
|
||
|
|
if (isNumeric(port.name)) {
|
||
|
|
this.registerDevToolsForTab(port);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (
|
||
|
|
!port.sender ||
|
||
|
|
!port.sender.tab ||
|
||
|
|
port.sender.tab.id === undefined ||
|
||
|
|
port.sender.frameId === undefined
|
||
|
|
) {
|
||
|
|
console.warn('Received a connection from an unknown sender', port);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
this.registerContentScriptForTab(port);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private ensureTabExists(tabId: number): void {
|
||
|
|
this.tabs[tabId] ??= {
|
||
|
|
devtools: null,
|
||
|
|
contentScripts: {},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
private registerDevToolsForTab(port: chrome.runtime.Port): void {
|
||
|
|
// For the devtools page, our port name is the tab id.
|
||
|
|
const tabId = parseInt(port.name, 10);
|
||
|
|
|
||
|
|
this.ensureTabExists(tabId);
|
||
|
|
const tab = this.tabs[tabId]!;
|
||
|
|
|
||
|
|
tab.devtools = port;
|
||
|
|
tab.devtools.onDisconnect.addListener(() => {
|
||
|
|
tab.devtools = null;
|
||
|
|
|
||
|
|
for (const connection of Object.values(tab.contentScripts)) {
|
||
|
|
connection.enabled = false;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// DevTools may register after the content script has already registered. If that's the case,
|
||
|
|
// we need to set up the double pipe between the devtools and each content script, and send
|
||
|
|
// the contentScriptConnected message to the devtools page to inform it of all frames on the page.
|
||
|
|
for (const [frameId, connection] of Object.entries(tab.contentScripts)) {
|
||
|
|
if (connection.port === null || connection.backendReady !== true) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
tab.devtools!.postMessage({
|
||
|
|
topic: 'contentScriptConnected',
|
||
|
|
args: [parseInt(frameId, 10), connection.port.name, connection.port.sender!.url],
|
||
|
|
});
|
||
|
|
this.doublePipe(tab.devtools, connection);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private registerContentScriptForTab(port: chrome.runtime.Port): void {
|
||
|
|
// A content script connection will have a sender and a tab id.
|
||
|
|
const sender = port.sender!;
|
||
|
|
const frameId = sender.frameId!;
|
||
|
|
const tabId = sender.tab!.id!;
|
||
|
|
|
||
|
|
this.ensureTabExists(tabId);
|
||
|
|
const tab = this.tabs[tabId]!;
|
||
|
|
|
||
|
|
if (tab.contentScripts[frameId] === undefined) {
|
||
|
|
tab.contentScripts[frameId] = {
|
||
|
|
port: null,
|
||
|
|
enabled: false,
|
||
|
|
frameId: -1,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const contentScript = tab.contentScripts[frameId]!;
|
||
|
|
contentScript.port = port;
|
||
|
|
contentScript.frameId = frameId;
|
||
|
|
contentScript.enabled = contentScript.enabled ?? false;
|
||
|
|
|
||
|
|
// When the content script disconnects, clean up the connection state we're storing in the
|
||
|
|
// background page.
|
||
|
|
contentScript.port.onDisconnect.addListener(() => {
|
||
|
|
delete tab.contentScripts[frameId];
|
||
|
|
|
||
|
|
if (Object.keys(tab.contentScripts).length === 0) {
|
||
|
|
delete this.tabs[tabId];
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Listen for the backendReady message from the content script. This message is sent when the
|
||
|
|
// content script has loaded the backend bundle and is ready to receive messages from the
|
||
|
|
// backend.
|
||
|
|
contentScript.port!.onMessage.addListener((message) => {
|
||
|
|
if (message.topic === 'backendReady') {
|
||
|
|
contentScript.backendReady = true;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// If the devtools connection is already established, set up the double pipe between the
|
||
|
|
// devtools and the content script.
|
||
|
|
if (tab.devtools) {
|
||
|
|
this.doublePipe(tab.devtools, tab.contentScripts[frameId]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private doublePipe(
|
||
|
|
devtoolsPort: chrome.runtime.Port | null,
|
||
|
|
contentScriptConnection: ContentScriptConnection,
|
||
|
|
): void {
|
||
|
|
if (devtoolsPort === null) {
|
||
|
|
throw new Error('DevTools port is equal to null');
|
||
|
|
}
|
||
|
|
|
||
|
|
const contentScriptPort = contentScriptConnection.port;
|
||
|
|
|
||
|
|
if (contentScriptPort === null) {
|
||
|
|
throw new Error('Content script port is equal to null');
|
||
|
|
}
|
||
|
|
|
||
|
|
// tslint:disable-next-line:no-console
|
||
|
|
console.log('Creating two-way communication channel', Date.now(), this.tabs);
|
||
|
|
|
||
|
|
const onDevToolsMessage = (message: {topic: Topic; args: Parameters<Events[Topic]>}) => {
|
||
|
|
if (message.topic === 'enableFrameConnection') {
|
||
|
|
if (message.args.length !== 2) {
|
||
|
|
throw new Error('Expected two arguments for enableFrameConnection');
|
||
|
|
}
|
||
|
|
|
||
|
|
const [frameId, tabId] = message.args as [frameId: number, tabId: number];
|
||
|
|
|
||
|
|
if (frameId === contentScriptConnection.frameId) {
|
||
|
|
const tab = this.tabs[tabId];
|
||
|
|
|
||
|
|
if (tab === undefined) {
|
||
|
|
throw new Error(`Expected tab to be registered with tabId ${tabId}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const frameId of Object.keys(tab.contentScripts)) {
|
||
|
|
tab.contentScripts[frameId].enabled = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
contentScriptConnection.enabled = true;
|
||
|
|
devtoolsPort.postMessage({
|
||
|
|
topic: 'frameConnected',
|
||
|
|
args: [contentScriptConnection.frameId],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Do not allow any message to be sent if a content script is not enabled. This is the
|
||
|
|
// mechanism that lets us select which content script connection Angular Devtools is connected
|
||
|
|
// to.
|
||
|
|
if (!contentScriptConnection.enabled) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
contentScriptPort.postMessage(message);
|
||
|
|
};
|
||
|
|
devtoolsPort.onMessage.addListener(onDevToolsMessage);
|
||
|
|
|
||
|
|
const onContentScriptMessage = (message: {topic: Topic; args: Parameters<Events[Topic]>}) => {
|
||
|
|
if (message.topic === 'backendReady') {
|
||
|
|
devtoolsPort.postMessage({
|
||
|
|
topic: 'contentScriptConnected',
|
||
|
|
args: [
|
||
|
|
contentScriptConnection.frameId,
|
||
|
|
contentScriptConnection.port!.name,
|
||
|
|
contentScriptConnection.port!.sender!.url,
|
||
|
|
],
|
||
|
|
});
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Do not allow any message to be sent if a content script is not enabled. This is the
|
||
|
|
// mechanism that lets us select which content script connection Angular Devtools is connected
|
||
|
|
// to.
|
||
|
|
if (!contentScriptConnection.enabled) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
devtoolsPort.postMessage(message);
|
||
|
|
};
|
||
|
|
contentScriptPort.onMessage.addListener(onContentScriptMessage);
|
||
|
|
|
||
|
|
const shutdownContentScript = () => {
|
||
|
|
devtoolsPort.onMessage.removeListener(onDevToolsMessage);
|
||
|
|
devtoolsPort.postMessage({
|
||
|
|
topic: 'contentScriptDisconnected',
|
||
|
|
args: [contentScriptConnection.frameId, contentScriptConnection.port!.name],
|
||
|
|
});
|
||
|
|
|
||
|
|
contentScriptPort.onMessage.removeListener(onContentScriptMessage);
|
||
|
|
contentScriptPort.disconnect();
|
||
|
|
};
|
||
|
|
|
||
|
|
contentScriptPort.onDisconnect.addListener(() => shutdownContentScript());
|
||
|
|
}
|
||
|
|
}
|