├── .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 | 
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 |
--------------------------------------------------------------------------------