├── .gitignore ├── img ├── readme.png └── Screenshots.png ├── src ├── extension │ ├── browserAction.html │ ├── index.css │ ├── manifest.json │ └── sandbox.html ├── sandbox │ ├── index.js │ ├── Puppeteer.js │ ├── Extension.ts │ └── Launcher.js ├── browserAction │ └── index.ts └── background │ └── index.ts ├── tsconfig.json ├── README.md ├── webpack.config.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /extension/ 2 | node_modules 3 | -------------------------------------------------------------------------------- /img/readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyo-ago/puppeteer-chrome-extension-player/HEAD/img/readme.png -------------------------------------------------------------------------------- /img/Screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyo-ago/puppeteer-chrome-extension-player/HEAD/img/Screenshots.png -------------------------------------------------------------------------------- /src/extension/browserAction.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "ES2015", 6 | "target": "ES2017", 7 | "allowJs": true, 8 | "sourceMap": true, 9 | "lib": ["DOM", "ES6"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/extension/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | } 4 | 5 | html, 6 | body, 7 | iframe { 8 | height: 100%; 9 | } 10 | iframe { 11 | width: 100%; 12 | } 13 | textarea { 14 | height: 80%; 15 | width: 100%; 16 | } 17 | button { 18 | position: absolute; 19 | right: 5px; 20 | } 21 | -------------------------------------------------------------------------------- /src/sandbox/index.js: -------------------------------------------------------------------------------- 1 | window.ws = WebSocket; 2 | window.mime = {}; 3 | 4 | window['require'] = mod => { 5 | return require('./puppeteer'); 6 | }; 7 | 8 | document.addEventListener('DOMContentLoaded', () => { 9 | document.querySelector('button').addEventListener('click', () => { 10 | let value = document.querySelector('textarea').value; 11 | eval(value); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Puppeteer Player for chrome extension 2 | 3 | Puppeteer API on the chrome extension. 4 | 5 | ![](img/readme.png) 6 | 7 | ## Install 8 | 9 | [puppeteer\-chrome\-extension\-player \- Chrome Web Store](https://chrome.google.com/webstore/detail/puppeteer-chrome-extensio/lllgpibegjejjepmpmmhhhnkdnpnchfb) 10 | 11 | ## Todo 12 | 13 | - [ ] Save screenshot. 14 | - [ ] Save PDF. 15 | 16 | ## License 17 | 18 | MIT 19 | -------------------------------------------------------------------------------- /src/extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppeteer-chrome-extension-player", 3 | "description": "puppeteer-chrome-extension-player", 4 | "version": "0.0.3", 5 | "permissions": ["debugger"], 6 | "background": { 7 | "scripts": ["background.js"], 8 | "persistent": false 9 | }, 10 | "browser_action": {}, 11 | "sandbox": { 12 | "pages": ["sandbox.html"] 13 | }, 14 | "manifest_version": 2 15 | } 16 | -------------------------------------------------------------------------------- /src/extension/sandbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: { 5 | background: './src/background/index.ts', 6 | browserAction: './src/browserAction/index.ts', 7 | sandbox: './src/sandbox/index.js', 8 | }, 9 | output: { 10 | path: path.resolve(__dirname, 'extension'), 11 | filename: '[name].js', 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.ts$/, 17 | use: 'ts-loader', 18 | exclude: /node_modules/, 19 | }, 20 | ], 21 | }, 22 | resolve: { 23 | extensions: ['.ts', '.js'], 24 | }, 25 | node: { 26 | fs: 'empty', 27 | }, 28 | devtool: 'inline-source-map', 29 | externals: { 30 | ws: 'ws', 31 | mime: 'mime', 32 | './BrowserFetcher': 'BrowserFetcher', 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /src/sandbox/Puppeteer.js: -------------------------------------------------------------------------------- 1 | const { helper } = require('../../node_modules/puppeteer-core/lib/helper'); 2 | const Launcher = require('./Launcher'); 3 | 4 | module.exports = class { 5 | /** 6 | * @param {!Object=} options 7 | * @return {!Promise} 8 | */ 9 | static launch(options) { 10 | return Launcher.launch(options); 11 | } 12 | 13 | /** 14 | * @param {{browserWSEndpoint: string, ignoreHTTPSErrors: boolean}} options 15 | * @return {!Promise} 16 | */ 17 | static connect(options) { 18 | return Launcher.connect(options); 19 | } 20 | 21 | /** 22 | * @return {string} 23 | */ 24 | static executablePath() { 25 | return Launcher.executablePath(); 26 | } 27 | 28 | /** 29 | * @return {!Array} 30 | */ 31 | static defaultArgs(options) { 32 | return Launcher.defaultArgs(options); 33 | } 34 | }; 35 | 36 | helper.tracePublicAPI(module.exports, 'Puppeteer'); 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kyo-ago/puppeteer-chrome-extension-player", 3 | "version": "0.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build": "rm -fr extension && mkdir -p extension && cp src/extension/* extension && webpack --config webpack.config.js --mode development", 7 | "prettier": "prettier --single-quote --trailing-comma es5 --write package.json tsconfig.json webpack.config.js src/*/**.{ts,js,css,json}", 8 | "deploy": "cd extension/ && rm -f Archive.zip && zip Archive.zip *", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "MIT", 13 | "dependencies": { 14 | "puppeteer-core": "^1.7.0" 15 | }, 16 | "devDependencies": { 17 | "@types/chrome": "0.0.72", 18 | "@types/node": "^10.9.2", 19 | "prettier": "1.14.2", 20 | "ts-loader": "^4.5.0", 21 | "typescript": "^3.0.1", 22 | "webpack": "^4.17.1", 23 | "webpack-cli": "^3.1.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/browserAction/index.ts: -------------------------------------------------------------------------------- 1 | let tabId: number, port: chrome.runtime.Port; 2 | window.addEventListener('message', async event => { 3 | let toSandbox = (message: any) => { 4 | (event.source).postMessage(message, '*'); 5 | }; 6 | 7 | if (event.data.type === 'connect') { 8 | tabId = await new Promise((resolve, reject) => { 9 | chrome.tabs.query({ active: true }, tabs => { 10 | if (chrome.runtime.lastError) { 11 | return reject(chrome.runtime.lastError); 12 | } 13 | resolve(tabs[0].id); 14 | }); 15 | }); 16 | port = chrome.runtime.connect(); 17 | port.onMessage.addListener(msg => { 18 | if (msg.type === 'created') { 19 | return toSandbox({ 20 | type: 'connected', 21 | }); 22 | } 23 | if (msg.type === 'result') { 24 | return toSandbox({ 25 | type: 'result', 26 | result: msg.result, 27 | }); 28 | } 29 | if (msg.type === 'onEvent') { 30 | return toSandbox({ 31 | type: 'onEvent', 32 | result: msg.result, 33 | }); 34 | } 35 | if (msg.type === 'disconnect') { 36 | return toSandbox({ 37 | type: 'disconnect', 38 | reason: msg.reason, 39 | }); 40 | } 41 | }); 42 | port.postMessage({ type: 'create', tabId }); 43 | return; 44 | } 45 | if (event.data.type === 'send') { 46 | console.assert(port, 'Invalid call sequence.'); 47 | port.postMessage({ type: 'send', message: event.data.message }); 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /src/sandbox/Extension.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | export default class Extension extends EventEmitter { 4 | constructor( 5 | private sendCall: (message: string) => void, 6 | private closeCall: () => void 7 | ) { 8 | super(); 9 | } 10 | static async create() { 11 | let extension: Extension; 12 | return new Promise(resolve => { 13 | window.addEventListener('message', event => { 14 | let throwMessage = (message: any) => { 15 | (event.source).postMessage(message, '*'); 16 | }; 17 | let data = event.data; 18 | if (data.type === 'connected') { 19 | extension = new Extension( 20 | (message: string) => { 21 | throwMessage({ 22 | type: 'send', 23 | message, 24 | }); 25 | }, 26 | () => { 27 | console.log('disconnected'); 28 | } 29 | ); 30 | return resolve(extension); 31 | } 32 | if (data.type === 'result') { 33 | return extension.emit('message', data.result); 34 | } 35 | if (data.type === 'onEvent') { 36 | return extension.emit('message', data.result); 37 | } 38 | if (data.type === 'disconnect') { 39 | console.log(data.reason); 40 | return extension.emit('close'); 41 | } 42 | }); 43 | window.parent.postMessage( 44 | { 45 | type: 'connect', 46 | }, 47 | '*' 48 | ); 49 | }); 50 | } 51 | 52 | async send(message: string) { 53 | this.sendCall(message); 54 | } 55 | 56 | close() { 57 | this.closeCall(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/sandbox/Launcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | const { 17 | Connection, 18 | } = require('../../node_modules/puppeteer-core/lib/Connection'); 19 | const { default: Extension } = require('./Extension'); 20 | const { Browser } = require('../../node_modules/puppeteer-core/lib/Browser'); 21 | const { debugError } = require('../../node_modules/puppeteer-core/lib/helper'); 22 | 23 | class Launcher { 24 | /** 25 | * @param {!(LaunchOptions & ChromeArgOptions & BrowserOptions)=} options 26 | * @return {!Promise} 27 | */ 28 | static async launch(options = {}) { 29 | return Launcher.connect({}); 30 | } 31 | /** 32 | * @param {!(BrowserOptions & {browserWSEndpoint: string})=} options 33 | * @return {!Promise} 34 | */ 35 | static async connect(options) { 36 | const { 37 | ignoreHTTPSErrors = false, 38 | defaultViewport = { width: 800, height: 600 }, 39 | slowMo = 0, 40 | } = options; 41 | let extension = await Extension.create(); 42 | const connection = new Connection('', extension, slowMo); 43 | const { browserContextIds } = await connection.send( 44 | 'Target.getBrowserContexts' 45 | ); 46 | return Browser.create( 47 | connection, 48 | browserContextIds, 49 | ignoreHTTPSErrors, 50 | defaultViewport, 51 | null, 52 | () => connection.send('Browser.close').catch(debugError) 53 | ); 54 | } 55 | } 56 | 57 | module.exports = Launcher; 58 | -------------------------------------------------------------------------------- /src/background/index.ts: -------------------------------------------------------------------------------- 1 | interface TargetInfo { 2 | /** Target type. */ 3 | type: string; 4 | /** Target id. */ 5 | id: string; 6 | /** 7 | * Optional. 8 | * Since Chrome 30. 9 | * The tab id, defined if type == 'page'. 10 | */ 11 | tabId?: number; 12 | /** 13 | * Optional. 14 | * Since Chrome 30. 15 | * The extension id, defined if type = 'background_page'. 16 | */ 17 | extensionId?: string; 18 | /** True if debugger is already attached. */ 19 | attached: boolean; 20 | /** Target page title. */ 21 | title: string; 22 | /** Target URL. */ 23 | url: string; 24 | /** Optional. Target favicon URL. */ 25 | faviconUrl?: string; 26 | } 27 | 28 | interface Debuggee { 29 | /** Optional. The id of the tab which you intend to debug. */ 30 | tabId?: number; 31 | /** 32 | * Optional. 33 | * Since Chrome 27. 34 | * The id of the extension which you intend to debug. Attaching to an extension background page is only possible when 'silent-debugger-extension-api' flag is enabled on the target browser. 35 | */ 36 | extensionId?: string; 37 | /** 38 | * Optional. 39 | * Since Chrome 28. 40 | * The opaque id of the debug target. 41 | */ 42 | targetId?: string; 43 | } 44 | 45 | class Background { 46 | private sessionId = 0; 47 | private targetInfo: TargetInfo[] = []; 48 | private onTarget: (method: string, targetInfo: TargetInfo) => void; 49 | private onDetachListener: Function | null = null; 50 | 51 | constructor(private debuggee: Debuggee) {} 52 | 53 | static async create(tabId: number): Promise { 54 | let debuggee = await Background.attach({ tabId }); 55 | return new Background(debuggee); 56 | } 57 | 58 | static attach(debuggee: Debuggee) { 59 | return new Promise((resolve, reject) => { 60 | chrome.debugger.attach(debuggee, '1.3', () => { 61 | if (chrome.runtime.lastError) { 62 | if ( 63 | chrome.runtime.lastError.message.match( 64 | /Another debugger is already attached/ 65 | ) 66 | ) { 67 | return resolve(debuggee); 68 | } 69 | return reject(chrome.runtime.lastError); 70 | } 71 | resolve(debuggee); 72 | }); 73 | }); 74 | } 75 | 76 | static detach(debuggee: Debuggee) { 77 | return new Promise((resolve, reject) => { 78 | chrome.debugger.detach(debuggee, () => { 79 | if (chrome.runtime.lastError) { 80 | return reject(chrome.runtime.lastError); 81 | } 82 | resolve(); 83 | }); 84 | }); 85 | } 86 | 87 | send(message: string): Promise { 88 | let { id, method, params } = JSON.parse(message); 89 | console.log('>>>', message); 90 | if (method === 'Target.setDiscoverTargets') { 91 | console.log('skip'); 92 | return this.setDiscoverTargets(id, method, params); 93 | } 94 | if (method === 'Target.attachToTarget') { 95 | console.log('skip'); 96 | return this.attachToTarget(id, method, params); 97 | } 98 | if (method === 'Target.sendMessageToTarget') { 99 | console.log('skip'); 100 | return this.sendMessageToTarget(id, method, params); 101 | } 102 | return this.sendMethod(id, method, params); 103 | } 104 | 105 | bindTarget(callback: (method: string, targetInfo: TargetInfo) => void) { 106 | this.onTarget = callback; 107 | } 108 | 109 | bindEvent(callback: (method: string, params: Object | null) => void) { 110 | chrome.debugger.onEvent.addListener((source, method, params) => { 111 | console.log('onEvent', source, method, params); 112 | if (!this.equalsDebuggee(source)) { 113 | return; 114 | } 115 | callback(method, params); 116 | }); 117 | } 118 | 119 | bindDetach(onDetach: (reason: string) => void) { 120 | this.onDetachListener = ((source: any, reason: string) => { 121 | console.log('onDetach', source, reason); 122 | if (!this.equalsDebuggee(source)) { 123 | return; 124 | } 125 | onDetach(reason); 126 | }).bind(this); 127 | this.detachAddListener(); 128 | } 129 | 130 | close() { 131 | return Background.detach(this.debuggee); 132 | } 133 | 134 | private async setDiscoverTargets(id: string, method: string, params: any) { 135 | await this.promisedTimeout(); 136 | await this.checkTargets(); 137 | let result = { id, result: {} }; 138 | console.log('<<<', result); 139 | return JSON.stringify(result); 140 | } 141 | 142 | private async attachToTarget(id: string, method: string, params: any) { 143 | await this.promisedTimeout(); 144 | await this.checkTargets(); 145 | this.detachRemoveListener(); 146 | 147 | await Background.detach(this.debuggee); 148 | let debuggee = await Background.attach({ targetId: params.targetId }); 149 | this.debuggee = debuggee; 150 | 151 | let result = { 152 | id, 153 | result: { 154 | sessionId: `ExtensionSessionId${++this.sessionId}`, 155 | }, 156 | }; 157 | console.log('<<<', result); 158 | return JSON.stringify(result); 159 | } 160 | 161 | private async sendMessageToTarget(id: string, method: string, params: any) { 162 | await this.promisedTimeout(); 163 | let message = JSON.parse(params.message); 164 | console.log('>>>', message); 165 | let result = await this.sendCommand( 166 | message.id, 167 | message.method, 168 | message.params 169 | ); 170 | await this.promisedTimeout(); 171 | console.log('<<<', JSON.stringify(result)); 172 | message.result = result; 173 | params.message = JSON.stringify(message); 174 | return JSON.stringify({ 175 | method: 'Target.receivedMessageFromTarget', 176 | params, 177 | }); 178 | } 179 | 180 | private async sendMethod(id: string, method: string, params: any) { 181 | let result = await this.sendCommand(id, method, params); 182 | if (method === 'Target.createTarget') { 183 | await this.checkTargets(); 184 | } 185 | let sendResult = JSON.stringify({ id, result }); 186 | console.log('<<<', sendResult); 187 | return sendResult; 188 | } 189 | 190 | private sendCommand(id: string, method: string, params: any) { 191 | return new Promise((resolve, reject) => { 192 | chrome.debugger.sendCommand(this.debuggee, method, params, result => { 193 | if (chrome.runtime.lastError) { 194 | return reject(chrome.runtime.lastError); 195 | } 196 | resolve(result); 197 | }); 198 | }); 199 | } 200 | 201 | private checkTargets() { 202 | return new Promise(resolve => { 203 | let emit = (method: string) => (targetInfo: TargetInfo) => { 204 | this.onTarget(method, targetInfo); 205 | }; 206 | let types = { 207 | created: emit('Target.targetCreated'), 208 | deleted: emit('Target.targetDestroyed'), 209 | changed: emit('Target.targetInfoChanged'), 210 | }; 211 | chrome.debugger.getTargets(targetInfoList => { 212 | let reducedInfo = this.targetInfo.reduce((base, cur) => { 213 | let filtered = targetInfoList.filter(diff => cur.id !== diff.id); 214 | if (filtered.length === targetInfoList.length) { 215 | types.deleted(cur); 216 | } else { 217 | base.push(cur); 218 | types.changed(cur); 219 | } 220 | targetInfoList = filtered; 221 | return base; 222 | }, []); 223 | targetInfoList.forEach(info => types.created(info)); 224 | this.targetInfo = reducedInfo.concat(targetInfoList); 225 | resolve(); 226 | }); 227 | }); 228 | } 229 | 230 | private promisedTimeout(time = 0) { 231 | return new Promise(resolve => setTimeout(resolve, time)); 232 | } 233 | 234 | private equalsDebuggee(debuggee: Debuggee): boolean { 235 | if (this.debuggee.tabId && debuggee.tabId) { 236 | return this.debuggee.tabId === debuggee.tabId; 237 | } 238 | if (this.debuggee.extensionId && debuggee.extensionId) { 239 | return this.debuggee.extensionId === debuggee.extensionId; 240 | } 241 | if (this.debuggee.targetId && debuggee.targetId) { 242 | return this.debuggee.targetId === debuggee.targetId; 243 | } 244 | return false; 245 | } 246 | 247 | private detachAddListener() { 248 | if (!this.onDetachListener) { 249 | return; 250 | } 251 | chrome.debugger.onDetach.addListener(this.onDetachListener); 252 | } 253 | 254 | private detachRemoveListener() { 255 | if (!this.onDetachListener) { 256 | return; 257 | } 258 | chrome.debugger.onDetach.removeListener(this.onDetachListener); 259 | } 260 | } 261 | 262 | chrome.runtime.onConnect.addListener((port: chrome.runtime.Port) => { 263 | let background: Background; 264 | port.onMessage.addListener(async msg => { 265 | if (msg.type === 'create') { 266 | background = await Background.create(msg.tabId).catch(error => { 267 | alert(error.message); 268 | return Promise.reject(error); 269 | }); 270 | background.bindTarget((method: string, targetInfo: TargetInfo) => { 271 | port.postMessage({ 272 | type: 'result', 273 | result: JSON.stringify({ 274 | method, 275 | params: { 276 | targetInfo: { 277 | ...targetInfo, 278 | targetId: targetInfo.id, 279 | }, 280 | }, 281 | }), 282 | }); 283 | }); 284 | background.bindEvent((method: string, params: Object | null) => { 285 | port.postMessage({ 286 | type: 'onEvent', 287 | result: JSON.stringify({ 288 | method, 289 | params, 290 | }), 291 | }); 292 | }); 293 | background.bindDetach((reason: string) => { 294 | port.postMessage({ 295 | type: 'disconnect', 296 | reason, 297 | }); 298 | }); 299 | port.postMessage({ type: 'created' }); 300 | return; 301 | } 302 | 303 | if (msg.type === 'send') { 304 | let result = await background.send(msg.message); 305 | port.postMessage({ 306 | type: 'result', 307 | result, 308 | }); 309 | return; 310 | } 311 | }); 312 | port.onDisconnect.addListener(() => { 313 | background.close(); 314 | }); 315 | }); 316 | 317 | chrome.browserAction.onClicked.addListener(tab => { 318 | window.open('browserAction.html', null, 'width=420,height=250'); 319 | }); 320 | --------------------------------------------------------------------------------