├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ ├── main.yml │ └── publish.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── devtools ├── .gclient ├── db.js ├── index.html ├── index.js ├── network.js ├── target.css ├── target.html └── target.js ├── karma.conf.js ├── package.json ├── prettier.config.js ├── script └── build.js ├── src ├── Chobitsu.ts ├── domains │ ├── CSS.ts │ ├── CacheStorage.ts │ ├── DOM.ts │ ├── DOMDebugger.ts │ ├── DOMStorage.ts │ ├── Debugger.ts │ ├── IndexedDB.ts │ ├── Input.ts │ ├── Network.ts │ ├── Overlay.ts │ ├── Page.ts │ ├── Runtime.ts │ └── Storage.ts ├── index.d.ts ├── index.ts └── lib │ ├── connector.ts │ ├── constants.ts │ ├── evaluate.ts │ ├── mutationObserver.ts │ ├── nodeManager.ts │ ├── objManager.ts │ ├── request.ts │ ├── resources.ts │ ├── scriptMananger.ts │ ├── stylesheet.ts │ └── util.ts ├── test ├── CSS.spec.js ├── DOM.spec.js ├── DOMDebugger.spec.js ├── DOMStorage.spec.js ├── Debugger.spec.js ├── Network.spec.js ├── Overlay.spec.js ├── Page.spec.js ├── Runtime.spec.js └── Storage.spec.js ├── tsconfig.json └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "parser": "@typescript-eslint/parser", 8 | "plugins": ["@typescript-eslint"], 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "prettier" 13 | ], 14 | "rules": { 15 | "@typescript-eslint/no-explicit-any": "off", 16 | "prefer-rest-params": "off", 17 | "@typescript-eslint/no-this-alias": "off", 18 | "@typescript-eslint/no-var-requires": "off", 19 | "@typescript-eslint/no-require-imports": "off", 20 | "@typescript-eslint/no-unused-vars": "off", 21 | "prefer-spread": "off" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: surunzi 2 | custom: [surunzi.com/wechatpay.html] -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - 'master' 8 | paths: 9 | - 'src/**/*' 10 | - 'test/**/*' 11 | 12 | jobs: 13 | ci: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: '18.x' 22 | - run: | 23 | npm i 24 | npm run ci 25 | - uses: codecov/codecov-action@v4 26 | with: 27 | token: ${{ secrets.CODECOV_TOKEN }} # required 28 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [created] 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: '18.x' 16 | registry-url: 'https://registry.npmjs.org' 17 | - run: | 18 | npm i 19 | npm run build 20 | - run: npm publish 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | coverage/ 4 | devtools/.cipd 5 | devtools/devtools-frontend 6 | devtools/.gclient_entries 7 | devtools/.gclient_previous_sync_commits 8 | package-lock.json -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.8.4 (18 Oct 2024) 2 | 3 | * fix: css priority [#16](https://github.com/liriliri/chobitsu/issues/16) 4 | 5 | ## 1.8.3 (17 Oct 2024) 6 | 7 | * fix: ignore special ws connection 8 | 9 | ## 1.8.2 (15 Oct 2024) 10 | 11 | * fix: cache network requests before enable 12 | * fix: fetch remains pending when error occurs [#10](https://github.com/liriliri/chobitsu/issues/10) 13 | 14 | ## 1.8.1 (27 Sep 2024) 15 | 16 | * fix: shadowRoot hook 17 | 18 | ## 1.8.0 (26 Sep 2024) 19 | 20 | * feat: support shadow dom 21 | 22 | ## 1.7.1 (3 Sep 2024) 23 | 24 | * fix: error caused by accessing object properties 25 | 26 | ## 1.7.0 (28 Aug 2024) 27 | 28 | * feat: support latest devtools 29 | 30 | ## 1.6.0 (20 Aug 2024) 31 | 32 | * feat: emulateTouchFromMouseEvent 33 | 34 | ## 1.5.1 (10 Aug 2024) 35 | 36 | * fix: WebSocket message base64 encoded 37 | 38 | ## 1.5.0 (31 Jul 2024) 39 | 40 | * feat: support IndexedDB 41 | * feat: support WebSocket 42 | 43 | ## 1.4.6 (4 Nov 2023) 44 | 45 | * fix: cache console logs before enable 46 | * fix: console complete 47 | 48 | ## 1.4.5 (1 Apr 2023) 49 | 50 | * fix: some console errors 51 | 52 | ## 1.4.4 (20 Dec 2022) 53 | 54 | * fix: select element on mobile 55 | 56 | ## 1.4.3 (17 Dec 2022) 57 | 58 | * feat: support DOM.getNode 59 | 60 | ## 1.4.2 (11 Dec 2022) 61 | 62 | * fix: dom highlighter style encapsulation 63 | 64 | ## 1.4.1 (2 Dec 2022) 65 | 66 | * feat: support android 5.0 67 | 68 | ## 1.4.0 (27 Nov 2022) 69 | 70 | * feat: support ie11 71 | 72 | ## 1.3.1 (21 Nov 2022) 73 | 74 | * fix: css variable and selector with comma #7 75 | 76 | ## 1.3.0 (15 Aug 2022) 77 | 78 | * feat: proxy support 79 | 80 | ## 1.2.1 (7 Aug 2022) 81 | 82 | * fix: css text empty if value contains special char 83 | * refactor: replace dom-to-image with html2canvas 84 | 85 | ## 1.2.0 (31 Jul 2022) 86 | 87 | * feat: support screencast 88 | 89 | ## 1.1.1 (30 Jul 2022) 90 | 91 | * fix: Page.getResourceTree url 92 | 93 | ## 1.1.0 (27 Jul 2022) 94 | 95 | * feat: sources support 96 | 97 | ## 1.0.1 (26 Jul 2022) 98 | 99 | * fix: redundant error log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-present liriliri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chobitsu 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Build status][ci-image]][ci-url] 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![License][license-image]][npm-url] 7 | 8 | [npm-image]: https://img.shields.io/npm/v/chobitsu?style=flat-square 9 | [npm-url]: https://npmjs.org/package/chobitsu 10 | [ci-image]: https://img.shields.io/github/actions/workflow/status/liriliri/chobitsu/main.yml?branch=master&style=flat-square 11 | [ci-url]: https://github.com/liriliri/chobitsu/actions/workflows/main.yml 12 | [codecov-image]: https://img.shields.io/codecov/c/github/liriliri/chobitsu?style=flat-square 13 | [codecov-url]: https://codecov.io/github/liriliri/chobitsu?branch=master 14 | [license-image]: https://img.shields.io/npm/l/chobitsu?style=flat-square 15 | 16 | [Chrome devtools protocol](https://chromedevtools.github.io/devtools-protocol/) JavaScript implementation. 17 | 18 | ## Install 19 | 20 | You can get it on npm. 21 | 22 | ```bash 23 | npm install chobitsu --save 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```javascript 29 | const chobitsu = require('chobitsu'); 30 | 31 | chobitsu.setOnMessage(message => { 32 | console.log(message); 33 | }); 34 | 35 | chobitsu.sendRawMessage(JSON.stringify({ 36 | id: 1, 37 | method: 'DOMStorage.clear', 38 | params: { 39 | storageId: { 40 | isLocalStorage: true, 41 | securityOrigin: 'http://example.com' 42 | } 43 | } 44 | })); 45 | ``` 46 | 47 | For more detailed usage instructions, please read the documentation at [chii.liriliri.io](https://chii.liriliri.io/docs/chobitsu.html)! 48 | -------------------------------------------------------------------------------- /devtools/.gclient: -------------------------------------------------------------------------------- 1 | solutions = [ 2 | { 3 | "name": "devtools-frontend", 4 | "url": "https://github.com/liriliri/devtools-frontend.git@origin/chii", 5 | "deps_file": "DEPS", 6 | "managed": False, 7 | "custom_deps": {}, 8 | }, 9 | ] 10 | -------------------------------------------------------------------------------- /devtools/db.js: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/enjalot/6472041 2 | var indexedDB = window.indexedDB 3 | var open = indexedDB.open('MyDatabase', 1) 4 | open.onupgradeneeded = function () { 5 | var db = open.result 6 | var store = db.createObjectStore('MyObjectStore', { keyPath: 'id' }) 7 | var index = store.createIndex('NameIndex', ['name.last', 'name.first']) 8 | } 9 | open.onsuccess = function () { 10 | var db = open.result 11 | var tx = db.transaction('MyObjectStore', 'readwrite') 12 | var store = tx.objectStore('MyObjectStore') 13 | var index = store.index('NameIndex') 14 | 15 | store.put({ id: 12345, name: { first: 'John', last: 'Doe' }, age: 42 }) 16 | store.put({ id: 67890, name: { first: 'Bob', last: 'Smith' }, age: 35 }) 17 | 18 | var getJohn = store.get(12345) 19 | var getBob = index.get(['Smith', 'Bob']) 20 | 21 | getJohn.onsuccess = function () { 22 | console.log(getJohn.result.name.first) // => "John" 23 | } 24 | 25 | getBob.onsuccess = function () { 26 | console.log(getBob.result.name.first) // => "Bob" 27 | } 28 | 29 | tx.oncomplete = function () { 30 | db.close() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /devtools/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chobitsu 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /devtools/index.js: -------------------------------------------------------------------------------- 1 | const targetIframe = document.getElementById('target') 2 | const devtoolsIframe = document.getElementById('devtools') 3 | 4 | function resetHeight() { 5 | const targetHeight = Math.floor(window.innerHeight * 0.4) 6 | targetIframe.style.height = targetHeight + 'px' 7 | devtoolsIframe.style.height = window.innerHeight - targetHeight + 'px' 8 | } 9 | 10 | resetHeight() 11 | 12 | window.addEventListener('resize', resetHeight) 13 | 14 | targetIframe.onload = function () { 15 | targetIframe.contentWindow.devtoolsIframe = devtoolsIframe 16 | } 17 | window.addEventListener('message', event => { 18 | targetIframe.contentWindow.postMessage(event.data, event.origin) 19 | }) 20 | 21 | const hostOrigin = location.protocol + '//' + location.host 22 | devtoolsIframe.src = `devtools-frontend/out/Default/gen/front_end/chii_app.html#?embedded=${hostOrigin}` 23 | -------------------------------------------------------------------------------- /devtools/network.js: -------------------------------------------------------------------------------- 1 | setTimeout(function () { 2 | fetch(location.href) 3 | }, 1000) 4 | 5 | function testFetch() { 6 | fetch('/index.js') 7 | fetch('https://domainnotexist.com').catch(err => { 8 | console.error(err) 9 | }) 10 | } 11 | 12 | function testXhr() { 13 | let xhr = new XMLHttpRequest() 14 | xhr.open('GET', '/index.js', true) 15 | xhr.send() 16 | 17 | xhr = new XMLHttpRequest() 18 | xhr.open('GET', 'https://domainnotexist.com', true) 19 | xhr.send() 20 | } 21 | 22 | function testWs() { 23 | const text = 'This is the text used for testing!' 24 | 25 | const enc = new TextEncoder() 26 | const ws = new WebSocket('wss://echo.websocket.org') 27 | 28 | ws.onopen = function () { 29 | ws.send(text) 30 | ws.send(enc.encode(text)) 31 | } 32 | setTimeout(() => { 33 | ws.close() 34 | }, 1000) 35 | 36 | const wsIgnore = new WebSocket( 37 | 'wss://echo.websocket.org?__chobitsu-hide__=true' 38 | ) 39 | 40 | wsIgnore.onopen = function () { 41 | wsIgnore.send(text) 42 | wsIgnore.send(enc.encode(text)) 43 | } 44 | setTimeout(() => { 45 | wsIgnore.close() 46 | }, 1000) 47 | } 48 | 49 | testFetch() 50 | testXhr() 51 | testWs() 52 | -------------------------------------------------------------------------------- /devtools/target.css: -------------------------------------------------------------------------------- 1 | .motto { 2 | display: inline-block; 3 | } 4 | 5 | .scroll { 6 | margin-top: 15px; 7 | border: 1px solid black; 8 | width: 400px; 9 | height: 200px; 10 | overflow: auto; 11 | } 12 | 13 | .big { 14 | width: 300px; 15 | height: 300px; 16 | margin-top: 15px; 17 | background-color: red; 18 | } 19 | 20 | .small { 21 | width: 100px; 22 | height: 100px; 23 | margin-top: 15px; 24 | background-color: blue; 25 | } 26 | 27 | .image { 28 | margin-top: 15px; 29 | } 30 | 31 | #priority.priority { 32 | background: orange; 33 | } 34 | 35 | #priority.priority { 36 | background: silver; 37 | } 38 | 39 | #priority.priority, .priority { 40 | background: tomato; 41 | } 42 | 43 | .priority { 44 | background: purple; 45 | } 46 | 47 | #priority { 48 | margin-top: 15px; 49 | width: 50px; 50 | height: 50px; 51 | background: green; 52 | } -------------------------------------------------------------------------------- /devtools/target.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Target 9 | 10 | 11 | 12 | 13 |
Hello Chii!
14 | 15 | 16 | 17 | 30 |
31 | 34 |
35 |
36 | 43 |
44 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. 45 | Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies 46 | sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, 47 | semper congue, euismod non, mi. Proin porttitor, orci nec nonummy 48 | molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. 49 | Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, 50 | enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. 51 | Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent 52 | blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. 53 | Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere 54 | cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque 55 | fermentum. Maecenas adipiscing ante non diam sodales hendrerit. 56 |
57 |
58 |
59 |
60 |
61 | placeholder 62 |
63 | 69 | 70 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /devtools/target.js: -------------------------------------------------------------------------------- 1 | import './db.js' 2 | import './network.js' 3 | 4 | const targetOrigin = location.protocol + '//' + location.host 5 | function sendToDevtools(message) { 6 | devtoolsIframe.contentWindow.postMessage( 7 | JSON.stringify(message), 8 | targetOrigin 9 | ) 10 | } 11 | let id = 1 12 | function sendToChobitsu(message) { 13 | message.id = 'tmp' + id++ 14 | chobitsu.sendRawMessage(JSON.stringify(message)) 15 | } 16 | chobitsu.setOnMessage(message => { 17 | if (message.indexOf('"id":"tmp') > -1) { 18 | return 19 | } 20 | devtoolsIframe.contentWindow.postMessage(message, targetOrigin) 21 | }) 22 | window.addEventListener('message', event => { 23 | if (event.origin !== targetOrigin) { 24 | return 25 | } 26 | if (event.data && typeof event.data === 'string') { 27 | chobitsu.sendRawMessage(event.data) 28 | } 29 | }) 30 | window.onload = function () { 31 | setTimeout(function () { 32 | if ( 33 | typeof devtoolsIframe !== 'undefined' && 34 | devtoolsIframe.contentWindow.runtime 35 | ) { 36 | resetDevtools() 37 | } 38 | }, 0) 39 | } 40 | function resetDevtools() { 41 | const window = devtoolsIframe.contentWindow 42 | setTimeout(() => { 43 | window.runtime.loadLegacyModule('core/sdk/sdk.js').then(SDKModule => { 44 | for (const resourceTreeModel of SDKModule.TargetManager.TargetManager.instance().models( 45 | SDKModule.ResourceTreeModel.ResourceTreeModel 46 | )) { 47 | resourceTreeModel.dispatchEventToListeners( 48 | SDKModule.ResourceTreeModel.Events.WillReloadPage, 49 | resourceTreeModel 50 | ) 51 | } 52 | sendToDevtools({ 53 | method: 'Page.frameNavigated', 54 | params: { 55 | frame: { 56 | id: '1', 57 | mimeType: 'text/html', 58 | securityOrigin: location.origin, 59 | url: location.href, 60 | }, 61 | type: 'Navigation', 62 | }, 63 | }) 64 | sendToChobitsu({ method: 'Network.enable' }) 65 | sendToDevtools({ method: 'Runtime.executionContextsCleared' }) 66 | sendToChobitsu({ method: 'Runtime.enable' }) 67 | sendToChobitsu({ method: 'Debugger.enable' }) 68 | sendToChobitsu({ method: 'DOMStorage.enable' }) 69 | sendToChobitsu({ method: 'DOM.enable' }) 70 | sendToChobitsu({ method: 'CSS.enable' }) 71 | sendToChobitsu({ method: 'Overlay.enable' }) 72 | sendToDevtools({ method: 'DOM.documentUpdated' }) 73 | sendToChobitsu({ method: 'Page.enable' }) 74 | sendToDevtools({ method: 'Page.loadEventFired' }) 75 | }) 76 | }, 0) 77 | } 78 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpackCfg = require('./webpack.config') 2 | webpackCfg.devtool = 'inline-source-map' 3 | webpackCfg.mode = 'development' 4 | webpackCfg.module.rules[0].use.unshift('@jsdevtools/coverage-istanbul-loader') 5 | 6 | module.exports = function (config) { 7 | config.set({ 8 | basePath: '', 9 | frameworks: ['mocha', 'chai'], 10 | files: [ 11 | 'src/index.ts', 12 | './test/CSS.spec.js', 13 | './test/Debugger.spec.js', 14 | './test/DOM.spec.js', 15 | './test/DOMDebugger.spec.js', 16 | './test/DOMStorage.spec.js', 17 | './test/Network.spec.js', 18 | './test/Overlay.spec.js', 19 | './test/Page.spec.js', 20 | './test/Runtime.spec.js', 21 | './test/Storage.spec.js', 22 | ], 23 | plugins: [ 24 | 'karma-mocha', 25 | 'karma-chai-plugins', 26 | 'karma-chrome-launcher', 27 | 'karma-webpack', 28 | 'karma-coverage-istanbul-reporter', 29 | ], 30 | webpackServer: { 31 | noInfo: true, 32 | }, 33 | webpack: webpackCfg, 34 | reporters: ['progress', 'coverage-istanbul'], 35 | coverageIstanbulReporter: { 36 | reports: ['html', 'lcovonly', 'text', 'text-summary'], 37 | }, 38 | preprocessors: { 39 | 'src/index.ts': ['webpack'], 40 | }, 41 | port: 9876, 42 | colors: true, 43 | logLevel: config.LOG_INFO, 44 | browsers: ['ChromeHeadless'], 45 | singleRun: true, 46 | concurrency: Infinity, 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chobitsu", 3 | "version": "1.8.4", 4 | "description": "Chrome devtools protocol JavaScript implementation", 5 | "main": "dist/chobitsu.js", 6 | "exports": { 7 | ".": "./dist/chobitsu.js", 8 | "./Chobitsu": "./dist/cjs/Chobitsu.js", 9 | "./domains/*": "./dist/cjs/domains/*" 10 | }, 11 | "files": [ 12 | "dist/*" 13 | ], 14 | "scripts": { 15 | "ci": "npm run lint && npm test && npm run build && npm run es5", 16 | "dev": "node script/build.js && concurrently \"tsc -w --inlineSourceMap\" \"webpack-dev-server --mode=development\"", 17 | "build": "rm -rf dist && tsc && webpack --mode=production && node script/build.js", 18 | "build:front_end": "cd devtools/devtools-frontend && gn gen out/Default --args=\"is_debug=false\" && autoninja -C out/Default", 19 | "lint": "eslint src/**/*.ts", 20 | "test": "karma start", 21 | "es5": "es-check es5 dist/**/*.js", 22 | "format": "lsla prettier \"src/**/*.ts\" \"*.{js,json}\" \"devtools/*.{js,html}\" \"test/*.js\" \"script/*.js\" --write", 23 | "init:front_end": "cd devtools && rm -rf devtools-frontend && gclient sync --with_branch_heads --verbose" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/liriliri/chobitsu.git" 28 | }, 29 | "keywords": [ 30 | "devtools" 31 | ], 32 | "author": "redhoodsu", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/liriliri/chobitsu/issues" 36 | }, 37 | "homepage": "https://github.com/liriliri/chobitsu#readme", 38 | "dependencies": { 39 | "axios": "^0.27.2", 40 | "core-js": "^3.26.1", 41 | "devtools-protocol": "^0.0.1339468", 42 | "html2canvas": "^1.4.1", 43 | "licia": "^1.41.1", 44 | "luna-dom-highlighter": "^1.0.2" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.17.10", 48 | "@babel/preset-env": "^7.17.10", 49 | "@jsdevtools/coverage-istanbul-loader": "^3.0.5", 50 | "@types/node": "^20.14.12", 51 | "@typescript-eslint/eslint-plugin": "^8.9.0", 52 | "@typescript-eslint/parser": "^8.9.0", 53 | "babel-loader": "^8.2.5", 54 | "concurrently": "^7.6.0", 55 | "es-check": "^7.2.1", 56 | "eslint": "^8.21.0", 57 | "eslint-config-prettier": "^8.5.0", 58 | "karma": "^6.3.19", 59 | "karma-chai-plugins": "^0.9.0", 60 | "karma-chrome-launcher": "^3.1.1", 61 | "karma-coverage-istanbul-reporter": "^3.0.3", 62 | "karma-mocha": "^2.0.1", 63 | "karma-webpack": "^5.0.1", 64 | "mocha": "^8.0.1", 65 | "raw-loader": "^4.0.2", 66 | "ts-loader": "^7.0.5", 67 | "tslint-config-prettier": "^1.18.0", 68 | "typescript": "^5.2.2", 69 | "webpack": "^5.93.0", 70 | "webpack-cli": "^5.1.4", 71 | "webpack-dev-server": "^5.0.4" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | arrowParens: 'avoid', 4 | semi: false, 5 | } 6 | -------------------------------------------------------------------------------- /script/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('licia/fs') 2 | const promisify = require('licia/promisify') 3 | const mkdir = promisify(require('licia/mkdir')) 4 | const path = require('path') 5 | 6 | async function copyDefinition() { 7 | const data = await fs.readFile(path.resolve(__dirname, '../src/index.d.ts')) 8 | const dist = path.resolve(__dirname, '../dist') 9 | if (!(await fs.exists(dist))) { 10 | await mkdir(dist) 11 | } 12 | await fs.writeFile(path.resolve(dist, 'chobitsu.d.ts'), data) 13 | } 14 | 15 | copyDefinition() 16 | -------------------------------------------------------------------------------- /src/Chobitsu.ts: -------------------------------------------------------------------------------- 1 | import connector from './lib/connector' 2 | import noop from 'licia/noop' 3 | import uuid from 'licia/uuid' 4 | import each from 'licia/each' 5 | import Emitter from 'licia/Emitter' 6 | import { ErrorWithCode } from './lib/util' 7 | import types from 'licia/types' 8 | 9 | type OnMessage = (message: string) => void 10 | type DomainMethod = (...args: any[]) => any 11 | 12 | export default class Chobitsu { 13 | private onMessage: OnMessage 14 | private resolves: Map void> = new Map() 15 | private domains: Map = new Map() 16 | constructor() { 17 | this.onMessage = noop 18 | connector.on('message', (message: any) => { 19 | const parsedMessage = JSON.parse(message) 20 | 21 | const resolve = this.resolves.get(parsedMessage.id) 22 | if (resolve) { 23 | resolve(parsedMessage.result) 24 | } 25 | 26 | if (!parsedMessage.id) { 27 | const [name, method] = parsedMessage.method.split('.') 28 | const domain = this.domains.get(name) 29 | if (domain) { 30 | domain.emit(method, parsedMessage.params) 31 | } 32 | } 33 | 34 | this.onMessage(message) 35 | }) 36 | } 37 | domain(name: string) { 38 | return this.domains.get(name) 39 | } 40 | setOnMessage(onMessage: OnMessage) { 41 | this.onMessage = onMessage 42 | } 43 | sendMessage(method: string, params: any = {}) { 44 | const id = uuid() 45 | 46 | this.sendRawMessage( 47 | JSON.stringify({ 48 | id, 49 | method, 50 | params, 51 | }) 52 | ) 53 | 54 | return new Promise(resolve => { 55 | this.resolves.set(id, resolve) 56 | }) 57 | } 58 | async sendRawMessage(message: string) { 59 | const parsedMessage = JSON.parse(message) 60 | 61 | const { method, params, id } = parsedMessage 62 | 63 | const resultMsg: any = { 64 | id, 65 | } 66 | 67 | try { 68 | resultMsg.result = await this.callMethod(method, params) 69 | } catch (e) { 70 | if (e instanceof ErrorWithCode) { 71 | resultMsg.error = { 72 | message: e.message, 73 | code: e.code, 74 | } 75 | } else if (e instanceof Error) { 76 | resultMsg.error = { 77 | message: e.message, 78 | } 79 | } 80 | } 81 | 82 | connector.emit('message', JSON.stringify(resultMsg)) 83 | } 84 | register(name: string, methods: types.PlainObj) { 85 | const domains = this.domains 86 | 87 | /* eslint-disable-next-line */ 88 | let domain = domains.get(name)! 89 | if (!domain) { 90 | domain = {} 91 | Emitter.mixin(domain) 92 | } 93 | each(methods, (fn: any, method: string) => { 94 | domain[method] = fn 95 | }) 96 | domains.set(name, domain) 97 | } 98 | private async callMethod(method: string, params: any) { 99 | const [domainName, methodName] = method.split('.') 100 | const domain = this.domain(domainName) 101 | if (domain) { 102 | if (domain[methodName]) { 103 | return domain[methodName](params) || {} 104 | } 105 | } 106 | 107 | throw Error(`${method} unimplemented`) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/domains/CSS.ts: -------------------------------------------------------------------------------- 1 | import { getNode, getNodeId } from '../lib/nodeManager' 2 | import * as stylesheet from '../lib/stylesheet' 3 | import map from 'licia/map' 4 | import last from 'licia/last' 5 | import each from 'licia/each' 6 | import trim from 'licia/trim' 7 | import selector from 'licia/selector' 8 | import startWith from 'licia/startWith' 9 | import concat from 'licia/concat' 10 | import escapeRegExp from 'licia/escapeRegExp' 11 | import connector from '../lib/connector' 12 | import mutationObserver from '../lib/mutationObserver' 13 | import { MAIN_FRAME_ID } from '../lib/constants' 14 | import Protocol from 'devtools-protocol' 15 | import CSS = Protocol.CSS 16 | 17 | let proxy = '' 18 | export function setProxy(params: any) { 19 | proxy = params.proxy 20 | } 21 | 22 | export function enable() { 23 | each(stylesheet.getStyleSheets(), (styleSheet: any) => { 24 | if (styleSheet.styleSheetId) { 25 | connector.trigger('CSS.styleSheetAdded', { 26 | header: { 27 | frameId: MAIN_FRAME_ID, 28 | isInline: false, 29 | styleSheetId: styleSheet.styleSheetId, 30 | sourceURL: styleSheet.href || '', 31 | startColumn: 0, 32 | startLine: 0, 33 | endColumn: 0, 34 | endLine: 0, 35 | }, 36 | }) 37 | } 38 | }) 39 | } 40 | 41 | export function getComputedStyleForNode( 42 | params: CSS.GetComputedStyleForNodeRequest 43 | ): CSS.GetComputedStyleForNodeResponse { 44 | const node = getNode(params.nodeId) 45 | 46 | const computedStyle: any = stylesheet.formatStyle( 47 | window.getComputedStyle(node) 48 | ) 49 | 50 | return { 51 | computedStyle: toCssProperties(computedStyle), 52 | } 53 | } 54 | 55 | export function getInlineStylesForNode( 56 | params: CSS.GetInlineStylesForNodeRequest 57 | ) { 58 | const { nodeId } = params 59 | const node = getNode(nodeId) 60 | const { style } = node 61 | const inlineStyle: any = { 62 | shorthandEntries: [], 63 | cssProperties: [], 64 | } 65 | 66 | if (style) { 67 | const styleSheetId = stylesheet.getOrCreateInlineStyleSheetId(nodeId) 68 | inlineStyle.styleSheetId = styleSheetId 69 | const cssText = node.getAttribute('style') || '' 70 | inlineStyle.cssText = cssText 71 | inlineStyle.range = { 72 | startLine: 0, 73 | startColumn: 0, 74 | endLine: getLineCount(cssText) - 1, 75 | endColumn: last(cssText.split('\n')).length, 76 | } 77 | let cssPropertiesWithRange = toCssProperties(parseCssText(cssText)) 78 | cssPropertiesWithRange = map(cssPropertiesWithRange, ({ name, value }) => { 79 | const { text, range } = getInlineStyleRange(name, value, cssText) 80 | 81 | const ret: any = { 82 | name, 83 | value, 84 | text, 85 | range, 86 | } 87 | 88 | if (startWith(text, '/*')) { 89 | ret.disabled = true 90 | } else { 91 | ret.disabled = false 92 | ret.implicit = false 93 | ret.parsedOk = style[name] !== '' 94 | } 95 | 96 | return ret 97 | }) 98 | const parsedStyle = stylesheet.formatStyle(style) 99 | each(cssPropertiesWithRange, ({ name }) => delete parsedStyle[name]) 100 | const cssPropertiesWithoutRange = toCssProperties(parsedStyle) 101 | 102 | inlineStyle.shorthandEntries = getShorthandEntries(style) 103 | inlineStyle.cssProperties = concat( 104 | cssPropertiesWithRange, 105 | cssPropertiesWithoutRange 106 | ) 107 | } 108 | 109 | return { 110 | inlineStyle, 111 | } 112 | } 113 | 114 | export function getMatchedStylesForNode( 115 | params: CSS.GetMatchedStylesForNodeRequest 116 | ): CSS.GetMatchedStylesForNodeResponse { 117 | const node = getNode(params.nodeId) 118 | const matchedCSSRules = stylesheet.getMatchedCssRules(node) 119 | 120 | return { 121 | matchedCSSRules: map(matchedCSSRules, matchedCSSRule => 122 | formatMatchedCssRule(node, matchedCSSRule) 123 | ), 124 | ...getInlineStylesForNode(params), 125 | } 126 | } 127 | 128 | export function getBackgroundColors( 129 | params: CSS.GetBackgroundColorsRequest 130 | ): CSS.GetBackgroundColorsResponse { 131 | const node = getNode(params.nodeId) 132 | 133 | const computedStyle: any = stylesheet.formatStyle( 134 | window.getComputedStyle(node) 135 | ) 136 | 137 | return { 138 | backgroundColors: [computedStyle['background-color']], 139 | computedFontSize: computedStyle['font-size'], 140 | computedFontWeight: computedStyle['font-weight'], 141 | } 142 | } 143 | 144 | export async function getStyleSheetText( 145 | params: CSS.GetStyleSheetTextRequest 146 | ): Promise { 147 | const { styleSheetId } = params 148 | 149 | const nodeId = stylesheet.getInlineStyleNodeId(styleSheetId) 150 | let text = '' 151 | if (nodeId) { 152 | const node = getNode(nodeId) 153 | text = node.getAttribute('style') || '' 154 | } else { 155 | text = await stylesheet.getStyleSheetText(styleSheetId, proxy) 156 | } 157 | 158 | return { 159 | text, 160 | } 161 | } 162 | 163 | export function setStyleTexts( 164 | params: CSS.SetStyleTextsRequest 165 | ): CSS.SetStyleTextsResponse { 166 | const { edits } = params 167 | const styles = map(edits, (edit: any) => { 168 | const { styleSheetId, text, range } = edit 169 | const nodeId = stylesheet.getInlineStyleNodeId(styleSheetId) 170 | // Only allow to edit inline style 171 | if (nodeId) { 172 | const node = getNode(nodeId) 173 | let cssText = node.getAttribute('style') || '' 174 | const { start, end } = getPosFromRange(range, cssText) 175 | cssText = cssText.slice(0, start) + text + cssText.slice(end) 176 | 177 | node.setAttribute('style', cssText) 178 | return getInlineStylesForNode({ nodeId }).inlineStyle 179 | } 180 | 181 | return { styleSheetId } 182 | }) 183 | 184 | return { 185 | styles, 186 | } 187 | } 188 | 189 | function splitSelector(selectorText: string) { 190 | const groups = selector.parse(selectorText) 191 | 192 | return map(groups, group => trim(selector.stringify([group]))) 193 | } 194 | 195 | function formatMatchedCssRule(node: any, matchedCssRule: any) { 196 | const { selectorText }: { selectorText: string } = matchedCssRule 197 | const selectors = splitSelector(selectorText) 198 | 199 | const shorthandEntries = getShorthandEntries(matchedCssRule.style) 200 | const style = stylesheet.formatStyle(matchedCssRule.style) 201 | 202 | const rule: any = { 203 | styleSheetId: matchedCssRule.styleSheetId, 204 | selectorList: { 205 | selectors: map(selectors, selector => ({ text: selector })), 206 | text: selectorText, 207 | }, 208 | style: { 209 | cssProperties: toCssProperties(style), 210 | shorthandEntries, 211 | }, 212 | } 213 | 214 | const matchingSelectors: number[] = [] 215 | each(selectors, (selector, idx) => { 216 | if (stylesheet.matchesSelector(node, selector)) { 217 | matchingSelectors.push(idx) 218 | } 219 | }) 220 | 221 | return { 222 | matchingSelectors, 223 | rule, 224 | } 225 | } 226 | 227 | stylesheet.onStyleSheetAdded((styleSheet: any) => { 228 | connector.trigger('CSS.styleSheetAdded', { 229 | header: { 230 | styleSheetId: styleSheet.styleSheetId, 231 | sourceURL: '', 232 | startColumn: 0, 233 | startLine: 0, 234 | endColumn: 0, 235 | endLine: 0, 236 | }, 237 | }) 238 | }) 239 | 240 | interface ICSSProperty { 241 | name: string 242 | value: string 243 | disabled?: boolean 244 | implicit?: boolean 245 | parsedOk?: boolean 246 | text?: string 247 | } 248 | 249 | function toCssProperties(style: any): ICSSProperty[] { 250 | const cssProperties: any[] = [] 251 | 252 | each(style, (value: string, name: string) => { 253 | cssProperties.push({ 254 | name, 255 | value, 256 | }) 257 | }) 258 | 259 | return cssProperties 260 | } 261 | 262 | function getLineCount(str: string) { 263 | return str.split('\n').length 264 | } 265 | 266 | const shortHandNames = ['background', 'font', 'border', 'margin', 'padding'] 267 | 268 | function getShorthandEntries(style: CSSStyleDeclaration) { 269 | const ret: any[] = [] 270 | 271 | each(shortHandNames, name => { 272 | const value = (style as any)[name] 273 | if (value) { 274 | ret.push({ 275 | name, 276 | value, 277 | }) 278 | } 279 | }) 280 | 281 | return ret 282 | } 283 | 284 | function parseCssText(cssText: string) { 285 | cssText = cssText.replace(/\/\*/g, '').replace(/\*\//g, '') 286 | const properties = cssText.split(';') 287 | const ret: any = {} 288 | 289 | each(properties, property => { 290 | property = trim(property) 291 | if (!property) return 292 | const colonPos = property.indexOf(':') 293 | if (colonPos) { 294 | const name = trim(property.slice(0, colonPos)) 295 | const value = trim(property.slice(colonPos + 1)) 296 | ret[name] = value 297 | } 298 | }) 299 | 300 | return ret 301 | } 302 | 303 | function getInlineStyleRange(name: string, value: string, cssText: string) { 304 | const lines = cssText.split('\n') 305 | let startLine = 0 306 | let endLine = 0 307 | let startColumn = 0 308 | let endColumn = 0 309 | let text = '' 310 | 311 | const reg = new RegExp( 312 | `(\\/\\*)?\\s*${escapeRegExp(name)}:\\s*${escapeRegExp( 313 | value 314 | )};?\\s*(\\*\\/)?` 315 | ) 316 | for (let i = 0, len = lines.length; i < len; i++) { 317 | const line = lines[i] 318 | const match = line.match(reg) 319 | if (match) { 320 | text = match[0] 321 | startLine = i 322 | startColumn = match.index || 0 323 | endLine = i 324 | endColumn = startColumn + text.length 325 | break 326 | } 327 | } 328 | 329 | return { 330 | range: { 331 | startLine, 332 | endLine, 333 | startColumn, 334 | endColumn, 335 | }, 336 | text, 337 | } 338 | } 339 | 340 | function getPosFromRange(range: any, cssText: string) { 341 | const { startLine, startColumn, endLine, endColumn } = range 342 | let start = 0 343 | let end = 0 344 | 345 | const lines = cssText.split('\n') 346 | for (let i = 0; i <= endLine; i++) { 347 | const line = lines[i] + 1 348 | const len = line.length 349 | if (i < startLine) { 350 | start += len 351 | } else if (i === startLine) { 352 | start += startColumn 353 | } 354 | if (i < endLine) { 355 | end += len 356 | } else if (i === endLine) { 357 | end += endColumn 358 | } 359 | } 360 | 361 | return { 362 | start, 363 | end, 364 | } 365 | } 366 | 367 | mutationObserver.on('attributes', (target: any, name: string) => { 368 | const nodeId = getNodeId(target) 369 | if (!nodeId) return 370 | if (name !== 'style') return 371 | 372 | const styleSheetId = stylesheet.getInlineStyleSheetId(nodeId) 373 | if (styleSheetId) { 374 | connector.trigger('CSS.styleSheetChanged', { 375 | styleSheetId, 376 | }) 377 | } 378 | }) 379 | -------------------------------------------------------------------------------- /src/domains/CacheStorage.ts: -------------------------------------------------------------------------------- 1 | import Protocol from 'devtools-protocol' 2 | import CacheStorage = Protocol.CacheStorage 3 | 4 | export function requestCacheNames(): CacheStorage.RequestCacheNamesResponse { 5 | return { 6 | caches: [], 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/domains/DOM.ts: -------------------------------------------------------------------------------- 1 | import connector from '../lib/connector' 2 | import * as nodeManager from '../lib/nodeManager' 3 | import { getNode, getNodeId } from '../lib/nodeManager' 4 | import * as objManager from '../lib/objManager' 5 | import mutationObserver from '../lib/mutationObserver' 6 | import $ from 'licia/$' 7 | import isNull from 'licia/isNull' 8 | import isEmpty from 'licia/isEmpty' 9 | import html from 'licia/html' 10 | import map from 'licia/map' 11 | import unique from 'licia/unique' 12 | import types from 'licia/types' 13 | import contain from 'licia/contain' 14 | import lowerCase from 'licia/lowerCase' 15 | import each from 'licia/each' 16 | import toArr from 'licia/toArr' 17 | import xpath from 'licia/xpath' 18 | import concat from 'licia/concat' 19 | import { setGlobal } from '../lib/evaluate' 20 | import { createId } from '../lib/util' 21 | import Protocol from 'devtools-protocol' 22 | import DOM = Protocol.DOM 23 | 24 | export function collectClassNamesFromSubtree( 25 | params: DOM.CollectClassNamesFromSubtreeRequest 26 | ): DOM.CollectClassNamesFromSubtreeResponse { 27 | const node = getNode(params.nodeId) 28 | 29 | const classNames: string[] = [] 30 | 31 | traverseNode(node, (node: any) => { 32 | if (node.nodeType !== 1) return 33 | const className = node.getAttribute('class') 34 | if (className) { 35 | const names = className.split(/\s+/) 36 | for (const name of names) classNames.push(name) 37 | } 38 | }) 39 | 40 | return { 41 | classNames: unique(classNames), 42 | } 43 | } 44 | 45 | export function copyTo(params: DOM.CopyToRequest): DOM.CopyToResponse { 46 | const { nodeId, targetNodeId } = params 47 | 48 | const node = getNode(nodeId) 49 | const targetNode = getNode(targetNodeId) 50 | 51 | const cloneNode = node.cloneNode(true) 52 | targetNode.appendChild(cloneNode) 53 | 54 | return { 55 | nodeId: getNodeId(cloneNode), 56 | } 57 | } 58 | 59 | let isEnable = false 60 | 61 | export function enable() { 62 | isEnable = true 63 | 64 | mutationObserver.disconnect() 65 | mutationObserver.observe(document.documentElement) 66 | nodeManager.clear() 67 | } 68 | 69 | function hookAttachShadow() { 70 | const origAttachShadow = Element.prototype.attachShadow 71 | if (origAttachShadow) { 72 | Element.prototype.attachShadow = function (init) { 73 | const shadowRoot = origAttachShadow.apply(this, [init]) 74 | if (!nodeManager.isValidNode(this)) { 75 | return shadowRoot 76 | } 77 | 78 | ;(this as any).chobitsuShadowRoot = shadowRoot 79 | if (isEnable) { 80 | mutationObserver.observe(shadowRoot) 81 | const hostId = getNodeId(this) 82 | if (hostId) { 83 | connector.trigger('DOM.shadowRootPushed', { 84 | hostId, 85 | root: nodeManager.wrap(shadowRoot, { depth: 1 }), 86 | }) 87 | } 88 | } 89 | return shadowRoot 90 | } 91 | } 92 | } 93 | 94 | hookAttachShadow() 95 | 96 | export function getDocument() { 97 | return { 98 | root: nodeManager.wrap(document, { 99 | depth: 2, 100 | }), 101 | } 102 | } 103 | 104 | export function getOuterHTML( 105 | params: DOM.GetOuterHTMLRequest 106 | ): DOM.GetOuterHTMLResponse { 107 | let outerHTML = '' 108 | 109 | if (params.nodeId) { 110 | const node = getNode(params.nodeId) 111 | outerHTML = node.outerHTML 112 | } 113 | 114 | return { 115 | outerHTML, 116 | } 117 | } 118 | 119 | export function moveTo(params: DOM.MoveToRequest): DOM.MoveToResponse { 120 | const { nodeId, targetNodeId } = params 121 | 122 | const node = getNode(nodeId) 123 | const targetNode = getNode(targetNodeId) 124 | 125 | targetNode.appendChild(node) 126 | 127 | return { 128 | nodeId: getNodeId(node), 129 | } 130 | } 131 | 132 | const searchResults = new Map() 133 | 134 | export function performSearch( 135 | params: DOM.PerformSearchRequest 136 | ): DOM.PerformSearchResponse { 137 | const query = lowerCase(params.query) 138 | let result: any[] = [] 139 | 140 | try { 141 | result = concat(result, toArr(document.querySelectorAll(query))) 142 | } catch (e) { 143 | /* tslint:disable-next-line */ 144 | } 145 | try { 146 | result = concat(result, xpath(query)) 147 | } catch (e) { 148 | /* tslint:disable-next-line */ 149 | } 150 | traverseNode(document, (node: any) => { 151 | const { nodeType } = node 152 | if (nodeType === 1) { 153 | const localName = node.localName 154 | if ( 155 | contain(`<${localName} `, query) || 156 | contain(``, query) 157 | ) { 158 | result.push(node) 159 | return 160 | } 161 | 162 | const attributes: string[] = [] 163 | each(node.attributes, ({ name, value }) => attributes.push(name, value)) 164 | for (let i = 0, len = attributes.length; i < len; i++) { 165 | if (contain(lowerCase(attributes[i]), query)) { 166 | result.push(node) 167 | break 168 | } 169 | } 170 | } else if (nodeType === 3) { 171 | if (contain(lowerCase(node.nodeValue), query)) { 172 | result.push(node) 173 | } 174 | } 175 | }) 176 | 177 | const searchId = createId() 178 | searchResults.set(searchId, result) 179 | 180 | return { 181 | searchId, 182 | resultCount: result.length, 183 | } 184 | } 185 | 186 | export function getSearchResults( 187 | params: DOM.GetSearchResultsRequest 188 | ): DOM.GetSearchResultsResponse { 189 | const { searchId, fromIndex, toIndex } = params 190 | 191 | const searchResult = searchResults.get(searchId) 192 | const result = searchResult.slice(fromIndex, toIndex) 193 | const nodeIds = map(result, (node: any) => { 194 | const nodeId = getNodeId(node) 195 | 196 | if (!nodeId) { 197 | return pushNodesToFrontend(node) 198 | } 199 | 200 | return nodeId 201 | }) 202 | 203 | return { 204 | nodeIds, 205 | } 206 | } 207 | 208 | // Make sure all parent nodes has been retrieved. 209 | export function pushNodesToFrontend(node: any) { 210 | const nodes = [node] 211 | let parentNode = node.parentNode 212 | while (parentNode) { 213 | nodes.push(parentNode) 214 | const nodeId = getNodeId(parentNode) 215 | if (nodeId) { 216 | break 217 | } else { 218 | parentNode = parentNode.parentNode 219 | } 220 | } 221 | while (nodes.length) { 222 | const node = nodes.pop() 223 | const nodeId = getNodeId(node) 224 | connector.trigger('DOM.setChildNodes', { 225 | parentId: nodeId, 226 | nodes: nodeManager.getChildNodes(node, 1), 227 | }) 228 | } 229 | 230 | return getNodeId(node) 231 | } 232 | 233 | export function discardSearchResults(params: DOM.DiscardSearchResultsRequest) { 234 | searchResults.delete(params.searchId) 235 | } 236 | 237 | export function pushNodesByBackendIdsToFrontend( 238 | params: DOM.PushNodesByBackendIdsToFrontendRequest 239 | ): DOM.PushNodesByBackendIdsToFrontendResponse { 240 | return { 241 | nodeIds: params.backendNodeIds, 242 | } 243 | } 244 | 245 | export function removeNode(params: DOM.RemoveNodeRequest) { 246 | const node = getNode(params.nodeId) 247 | 248 | $(node).remove() 249 | } 250 | 251 | export function requestChildNodes(params: DOM.RequestChildNodesRequest) { 252 | const { nodeId, depth = 1 } = params 253 | const node = getNode(nodeId) 254 | 255 | connector.trigger('DOM.setChildNodes', { 256 | parentId: nodeId, 257 | nodes: nodeManager.getChildNodes(node, depth), 258 | }) 259 | } 260 | 261 | export function requestNode( 262 | params: DOM.RequestNodeRequest 263 | ): DOM.RequestNodeResponse { 264 | const node = objManager.getObj(params.objectId) 265 | 266 | return { 267 | nodeId: getNodeId(node), 268 | } 269 | } 270 | 271 | export function resolveNode( 272 | params: DOM.ResolveNodeRequest 273 | ): DOM.ResolveNodeResponse { 274 | const node = getNode(params.nodeId as number) 275 | 276 | return { 277 | object: objManager.wrap(node), 278 | } 279 | } 280 | 281 | export function setAttributesAsText(params: DOM.SetAttributesAsTextRequest) { 282 | const { name, text, nodeId } = params 283 | 284 | const node = getNode(nodeId) 285 | if (name) { 286 | node.removeAttribute(name) 287 | } 288 | $(node).attr(parseAttributes(text)) 289 | } 290 | 291 | export function setAttributeValue(params: DOM.SetAttributeValueRequest) { 292 | const { nodeId, name, value } = params 293 | const node = getNode(nodeId) 294 | node.setAttribute(name, value) 295 | } 296 | 297 | const history: any[] = [] 298 | 299 | export function setInspectedNode(params: DOM.SetInspectedNodeRequest) { 300 | const node = getNode(params.nodeId) 301 | history.unshift(node) 302 | if (history.length > 5) history.pop() 303 | for (let i = 0; i < 5; i++) { 304 | setGlobal(`$${i}`, history[i]) 305 | } 306 | } 307 | 308 | export function setNodeValue(params: DOM.SetNodeValueRequest) { 309 | const { nodeId, value } = params 310 | const node = getNode(nodeId) 311 | node.nodeValue = value 312 | } 313 | 314 | export function setOuterHTML(params: DOM.SetOuterHTMLRequest) { 315 | const { nodeId, outerHTML } = params 316 | 317 | const node = getNode(nodeId) 318 | node.outerHTML = outerHTML 319 | } 320 | 321 | export function getDOMNodeId(params: any) { 322 | const { node } = params 323 | return { 324 | nodeId: nodeManager.getOrCreateNodeId(node), 325 | } 326 | } 327 | 328 | export function getDOMNode(params: any) { 329 | const { nodeId } = params 330 | 331 | return { 332 | node: getNode(nodeId), 333 | } 334 | } 335 | 336 | export function getTopLayerElements(): DOM.GetTopLayerElementsResponse { 337 | return { 338 | nodeIds: [], 339 | } 340 | } 341 | 342 | export function getNodesForSubtreeByStyle(): DOM.GetNodesForSubtreeByStyleResponse { 343 | return { 344 | nodeIds: [], 345 | } 346 | } 347 | 348 | function parseAttributes(str: string) { 349 | str = `
` 350 | 351 | return html.parse(str)[0].attrs 352 | } 353 | 354 | function traverseNode(node: any, cb: types.AnyFn) { 355 | const childNodes = nodeManager.filterNodes(node.childNodes) 356 | for (let i = 0, len = childNodes.length; i < len; i++) { 357 | const child = childNodes[i] 358 | cb(child) 359 | traverseNode(child, cb) 360 | } 361 | } 362 | 363 | mutationObserver.on('attributes', (target: any, name: string) => { 364 | const nodeId = getNodeId(target) 365 | if (!nodeId) return 366 | 367 | const value = target.getAttribute(name) 368 | 369 | if (isNull(value)) { 370 | connector.trigger('DOM.attributeRemoved', { 371 | nodeId, 372 | name, 373 | }) 374 | } else { 375 | connector.trigger('DOM.attributeModified', { 376 | nodeId, 377 | name, 378 | value, 379 | }) 380 | } 381 | }) 382 | 383 | mutationObserver.on( 384 | 'childList', 385 | (target: Node, addedNodes: NodeList, removedNodes: NodeList) => { 386 | const parentNodeId = getNodeId(target) 387 | if (!parentNodeId) return 388 | 389 | addedNodes = nodeManager.filterNodes(addedNodes) 390 | removedNodes = nodeManager.filterNodes(removedNodes) 391 | 392 | function childNodeCountUpdated() { 393 | connector.trigger('DOM.childNodeCountUpdated', { 394 | childNodeCount: nodeManager.wrap(target, { 395 | depth: 0, 396 | }).childNodeCount, 397 | nodeId: parentNodeId, 398 | }) 399 | } 400 | 401 | if (!isEmpty(addedNodes)) { 402 | childNodeCountUpdated() 403 | for (let i = 0, len = addedNodes.length; i < len; i++) { 404 | const node = addedNodes[i] 405 | const previousNode = nodeManager.getPreviousNode(node) 406 | const previousNodeId = previousNode ? getNodeId(previousNode) : 0 407 | const params: any = { 408 | node: nodeManager.wrap(node, { 409 | depth: 0, 410 | }), 411 | parentNodeId, 412 | previousNodeId, 413 | } 414 | 415 | connector.trigger('DOM.childNodeInserted', params) 416 | } 417 | } 418 | 419 | if (!isEmpty(removedNodes)) { 420 | for (let i = 0, len = removedNodes.length; i < len; i++) { 421 | const node = removedNodes[i] 422 | const nodeId = getNodeId(node) 423 | if (!nodeId) { 424 | childNodeCountUpdated() 425 | break 426 | } 427 | connector.trigger('DOM.childNodeRemoved', { 428 | nodeId: getNodeId(node), 429 | parentNodeId, 430 | }) 431 | } 432 | } 433 | } 434 | ) 435 | 436 | mutationObserver.on('characterData', (target: Node) => { 437 | const nodeId = getNodeId(target) 438 | if (!nodeId) return 439 | 440 | connector.trigger('DOM.characterDataModified', { 441 | characterData: target.nodeValue, 442 | nodeId, 443 | }) 444 | }) 445 | -------------------------------------------------------------------------------- /src/domains/DOMDebugger.ts: -------------------------------------------------------------------------------- 1 | import safeGet from 'licia/safeGet' 2 | import isEl from 'licia/isEl' 3 | import isFn from 'licia/isFn' 4 | import isBool from 'licia/isBool' 5 | import keys from 'licia/keys' 6 | import each from 'licia/each' 7 | import defaults from 'licia/defaults' 8 | import isObj from 'licia/isObj' 9 | import * as objManager from '../lib/objManager' 10 | import Protocol from 'devtools-protocol' 11 | import DOMDebugger = Protocol.DOMDebugger 12 | 13 | export function getEventListeners( 14 | params: DOMDebugger.GetEventListenersRequest 15 | ): DOMDebugger.GetEventListenersResponse { 16 | const obj = objManager.getObj(params.objectId) 17 | 18 | const events = obj.chobitsuEvents || [] 19 | const listeners: any[] = [] 20 | 21 | each(events, (events: any[], type) => { 22 | each(events, event => { 23 | listeners.push({ 24 | type, 25 | useCapture: event.useCapture, 26 | handler: objManager.wrap(event.listener), 27 | passive: event.passive, 28 | once: event.once, 29 | scriptId: '1', 30 | columnNumber: 0, 31 | lineNumber: 0, 32 | }) 33 | }) 34 | }) 35 | 36 | return { 37 | listeners, 38 | } 39 | } 40 | 41 | const getWinEventProto = () => { 42 | return safeGet(window, 'EventTarget.prototype') || window.Node.prototype 43 | } 44 | 45 | const winEventProto = getWinEventProto() 46 | 47 | const origAddEvent = winEventProto.addEventListener 48 | const origRmEvent = winEventProto.removeEventListener 49 | 50 | winEventProto.addEventListener = function ( 51 | type: string, 52 | listener: any, 53 | options: any 54 | ) { 55 | addEvent(this, type, listener, options) 56 | origAddEvent.apply(this, arguments) 57 | } 58 | 59 | winEventProto.removeEventListener = function (type: string, listener: any) { 60 | rmEvent(this, type, listener) 61 | origRmEvent.apply(this, arguments) 62 | } 63 | 64 | function addEvent(el: any, type: string, listener: any, options: any = false) { 65 | if (!isEl(el) || !isFn(listener)) return 66 | 67 | if (isBool(options)) { 68 | options = { 69 | capture: options, 70 | } 71 | } else if (!isObj(options)) { 72 | options = {} 73 | } 74 | defaults(options, { 75 | capture: false, 76 | passive: false, 77 | once: false, 78 | }) 79 | 80 | const events = ((el as any).chobitsuEvents = (el as any).chobitsuEvents || {}) 81 | 82 | events[type] = events[type] || [] 83 | events[type].push({ 84 | listener, 85 | useCapture: options.capture, 86 | passive: options.passive, 87 | once: options.once, 88 | }) 89 | } 90 | 91 | function rmEvent(el: any, type: string, listener: any) { 92 | if (!isEl(el) || !isFn(listener)) return 93 | 94 | const events = (el as any).chobitsuEvents 95 | 96 | if (!(events && events[type])) return 97 | 98 | const listeners = events[type] 99 | 100 | for (let i = 0, len = listeners.length; i < len; i++) { 101 | if (listeners[i].listener === listener) { 102 | listeners.splice(i, 1) 103 | break 104 | } 105 | } 106 | 107 | if (listeners.length === 0) delete events[type] 108 | if (keys(events).length === 0) delete (el as any).chobitsuEvents 109 | } 110 | -------------------------------------------------------------------------------- /src/domains/DOMStorage.ts: -------------------------------------------------------------------------------- 1 | import safeStorage from 'licia/safeStorage' 2 | import each from 'licia/each' 3 | import isStr from 'licia/isStr' 4 | import once from 'licia/once' 5 | import jsonClone from 'licia/jsonClone' 6 | import connector from '../lib/connector' 7 | import detectBrowser from 'licia/detectBrowser' 8 | import Protocol from 'devtools-protocol' 9 | import DOMStorage = Protocol.DOMStorage 10 | 11 | const localStore = safeStorage('local') 12 | const sessionStore = safeStorage('session') 13 | const browser = detectBrowser() 14 | 15 | export function clear(params: DOMStorage.ClearRequest) { 16 | const store = getStore(params.storageId) 17 | 18 | store.clear() 19 | } 20 | 21 | export function getDOMStorageItems( 22 | params: DOMStorage.GetDOMStorageItemsRequest 23 | ): DOMStorage.GetDOMStorageItemsResponse { 24 | const store = getStore(params.storageId) 25 | 26 | const entries: string[][] = [] 27 | 28 | each(jsonClone(store), (val, key: string) => { 29 | if (!isStr(val)) return 30 | 31 | entries.push([key, val]) 32 | }) 33 | 34 | return { 35 | entries, 36 | } 37 | } 38 | 39 | export function removeDOMStorageItem( 40 | params: DOMStorage.RemoveDOMStorageItemRequest 41 | ) { 42 | const { key, storageId } = params 43 | 44 | const store = getStore(storageId) 45 | 46 | store.removeItem(key) 47 | } 48 | 49 | export function setDOMStorageItem(params: DOMStorage.SetDOMStorageItemRequest) { 50 | const { key, value, storageId } = params 51 | 52 | const store = getStore(storageId) 53 | 54 | store.setItem(key, value) 55 | } 56 | 57 | export const enable = once(function () { 58 | // IE localStorage is incorrectly implemented. 59 | if (browser.name === 'ie') { 60 | return 61 | } 62 | each(['local', 'session'], type => { 63 | const store = type === 'local' ? localStore : sessionStore 64 | const storageId = getStorageId(type) 65 | 66 | const originSetItem = store.setItem.bind(store) 67 | store.setItem = function (key: string, value: string) { 68 | if (!isStr(key) || !isStr(value)) return 69 | 70 | const oldValue = store.getItem(key) 71 | originSetItem(key, value) 72 | if (oldValue) { 73 | connector.trigger('DOMStorage.domStorageItemUpdated', { 74 | key, 75 | newValue: value, 76 | oldValue, 77 | storageId, 78 | }) 79 | } else { 80 | connector.trigger('DOMStorage.domStorageItemAdded', { 81 | key, 82 | newValue: value, 83 | storageId, 84 | }) 85 | } 86 | } 87 | 88 | const originRemoveItem = store.removeItem.bind(store) 89 | store.removeItem = function (key: string) { 90 | if (!isStr(key)) return 91 | const oldValue = store.getItem(key) 92 | if (oldValue) { 93 | originRemoveItem(key) 94 | connector.trigger('DOMStorage.domStorageItemRemoved', { 95 | key, 96 | storageId, 97 | }) 98 | } 99 | } 100 | 101 | const originClear = store.clear.bind(store) 102 | store.clear = function () { 103 | originClear() 104 | connector.trigger('DOMStorage.domStorageItemsCleared', { 105 | storageId, 106 | }) 107 | } 108 | }) 109 | }) 110 | 111 | function getStorageId(type: string) { 112 | return { 113 | securityOrigin: location.origin, 114 | isLocalStorage: type === 'local', 115 | } 116 | } 117 | 118 | function getStore(storageId: any) { 119 | const { isLocalStorage } = storageId 120 | 121 | return isLocalStorage ? localStore : sessionStore 122 | } 123 | -------------------------------------------------------------------------------- /src/domains/Debugger.ts: -------------------------------------------------------------------------------- 1 | import connector from '../lib/connector' 2 | import * as scriptMananger from '../lib/scriptMananger' 3 | import each from 'licia/each' 4 | import Protocol from 'devtools-protocol' 5 | import Debugger = Protocol.Debugger 6 | 7 | let proxy = '' 8 | 9 | export function setProxy(params: any) { 10 | proxy = params.proxy 11 | } 12 | 13 | export function enable() { 14 | each(scriptMananger.getScripts(), script => { 15 | connector.trigger('Debugger.scriptParsed', script) 16 | }) 17 | } 18 | 19 | export async function getScriptSource( 20 | params: Debugger.GetScriptSourceRequest 21 | ): Promise { 22 | return { 23 | scriptSource: await scriptMananger.getScriptSource(params.scriptId, proxy), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/domains/IndexedDB.ts: -------------------------------------------------------------------------------- 1 | import each from 'licia/each' 2 | import map from 'licia/map' 3 | import isStr from 'licia/isStr' 4 | import isArr from 'licia/isArr' 5 | import * as objManager from '../lib/objManager' 6 | import Protocol from 'devtools-protocol' 7 | import IndexedDB = Protocol.IndexedDB 8 | 9 | const indexedDB = window.indexedDB 10 | 11 | let databaseVersions: any = {} 12 | 13 | export async function requestDatabaseNames(): Promise { 14 | const databases = await indexedDB.databases() 15 | const databaseNames: string[] = [] 16 | 17 | databaseVersions = {} 18 | each(databases, database => { 19 | if (!database.name) { 20 | return 21 | } 22 | databaseVersions[database.name] = database.version 23 | databaseNames.push(database.name) 24 | }) 25 | 26 | return { 27 | databaseNames, 28 | } 29 | } 30 | 31 | export async function requestDatabase( 32 | params: IndexedDB.RequestDatabaseRequest 33 | ): Promise { 34 | const { databaseName } = params 35 | const version = databaseVersions[databaseName] 36 | const objectStores: any[] = [] 37 | 38 | const db = await promisify(indexedDB.open(databaseName)) 39 | if (db.objectStoreNames.length) { 40 | const storeList = map(db.objectStoreNames, name => { 41 | return db.transaction(name, 'readonly').objectStore(name) 42 | }) 43 | each(storeList, store => { 44 | const indexes: any[] = [] 45 | each(store.indexNames, indexName => { 46 | const index = store.index(indexName) 47 | indexes.push({ 48 | name: index.name, 49 | multiEntry: index.multiEntry, 50 | keyPath: formatKeyPath(index.keyPath), 51 | unique: index.unique, 52 | }) 53 | }) 54 | objectStores.push({ 55 | name: store.name, 56 | indexes, 57 | keyPath: formatKeyPath(store.keyPath), 58 | autoIncrement: store.autoIncrement, 59 | }) 60 | }) 61 | } 62 | 63 | return { 64 | databaseWithObjectStores: { 65 | name: databaseName, 66 | objectStores, 67 | version, 68 | }, 69 | } 70 | } 71 | 72 | export async function requestData( 73 | params: IndexedDB.RequestDataRequest 74 | ): Promise { 75 | const { databaseName, objectStoreName, indexName, pageSize, skipCount } = 76 | params 77 | 78 | const db = await promisify(indexedDB.open(databaseName)) 79 | const objectStore = db 80 | .transaction(objectStoreName, 'readonly') 81 | .objectStore(objectStoreName) 82 | const count = await promisify(objectStore.count()) 83 | 84 | let currentIdx = 0 85 | let cursorRequest: IDBRequest 86 | if (indexName) { 87 | const index = objectStore.index(indexName) 88 | cursorRequest = index.openCursor() 89 | } else { 90 | cursorRequest = objectStore.openCursor() 91 | } 92 | return new Promise((resolve, reject) => { 93 | const objectStoreDataEntries: any[] = [] 94 | cursorRequest.addEventListener('success', () => { 95 | const cursor = cursorRequest.result 96 | if (cursor && currentIdx < pageSize + skipCount) { 97 | if (currentIdx >= skipCount) { 98 | objectStoreDataEntries.push({ 99 | key: objManager.wrap(cursor.key, { 100 | generatePreview: true, 101 | }), 102 | primaryKey: objManager.wrap(cursor.primaryKey), 103 | value: objManager.wrap(cursor.value, { 104 | generatePreview: true, 105 | }), 106 | }) 107 | } 108 | cursor.continue() 109 | currentIdx++ 110 | } else { 111 | resolve({ 112 | hasMore: currentIdx < count, 113 | objectStoreDataEntries, 114 | }) 115 | } 116 | }) 117 | cursorRequest.addEventListener('error', reject) 118 | }) 119 | } 120 | 121 | export async function getMetadata( 122 | params: IndexedDB.GetMetadataRequest 123 | ): Promise { 124 | const { databaseName, objectStoreName } = params 125 | 126 | const objectStore = await getObjectStore(databaseName, objectStoreName) 127 | 128 | return { 129 | entriesCount: await promisify(objectStore.count()), 130 | keyGeneratorValue: 1, 131 | } 132 | } 133 | 134 | export async function deleteObjectStoreEntries( 135 | params: IndexedDB.DeleteObjectStoreEntriesRequest 136 | ) { 137 | const { databaseName, objectStoreName, keyRange } = params 138 | 139 | const objectStore = await getObjectStore(databaseName, objectStoreName) 140 | await promisify( 141 | objectStore.delete( 142 | IDBKeyRange.bound( 143 | getKeyRangeBound(keyRange.lower), 144 | getKeyRangeBound(keyRange.upper), 145 | keyRange.lowerOpen, 146 | keyRange.upperOpen 147 | ) 148 | ) 149 | ) 150 | } 151 | 152 | export async function clearObjectStore( 153 | params: IndexedDB.ClearObjectStoreRequest 154 | ) { 155 | const { databaseName, objectStoreName } = params 156 | 157 | const objectStore = await getObjectStore(databaseName, objectStoreName) 158 | await promisify(objectStore.clear()) 159 | } 160 | 161 | export async function deleteDatabase(params: IndexedDB.DeleteDatabaseRequest) { 162 | await promisify(indexedDB.deleteDatabase(params.databaseName)) 163 | } 164 | 165 | function getKeyRangeBound(key: any) { 166 | return key.number || key.string || key.date || key.array || key 167 | } 168 | 169 | async function getObjectStore(databaseName: string, objectStoreName: string) { 170 | const db = await promisify(indexedDB.open(databaseName)) 171 | const objectStore = db 172 | .transaction(objectStoreName, 'readwrite') 173 | .objectStore(objectStoreName) 174 | 175 | return objectStore 176 | } 177 | 178 | function formatKeyPath(keyPath: any) { 179 | if (isStr(keyPath)) { 180 | return { 181 | type: 'string', 182 | string: keyPath, 183 | } 184 | } 185 | 186 | if (isArr(keyPath)) { 187 | return { 188 | type: 'array', 189 | array: keyPath, 190 | } 191 | } 192 | 193 | return { 194 | type: 'null', 195 | } 196 | } 197 | 198 | function promisify(req: IDBRequest): Promise { 199 | return new Promise((resolve, reject) => { 200 | req.addEventListener('success', () => { 201 | resolve(req.result) 202 | }) 203 | req.addEventListener('error', () => { 204 | reject() 205 | }) 206 | }) 207 | } 208 | -------------------------------------------------------------------------------- /src/domains/Input.ts: -------------------------------------------------------------------------------- 1 | import isUndef from 'licia/isUndef' 2 | import toBool from 'licia/toBool' 3 | import Protocol from 'devtools-protocol' 4 | import Input = Protocol.Input 5 | 6 | let isClick = false 7 | 8 | export function emulateTouchFromMouseEvent( 9 | params: Input.EmulateTouchFromMouseEventRequest 10 | ) { 11 | const { type, x, y, deltaX, deltaY } = params 12 | 13 | const el = document.elementFromPoint(x, y) || document.documentElement 14 | 15 | switch (type) { 16 | case 'mousePressed': 17 | isClick = true 18 | triggerTouchEvent('touchstart', el, x, y) 19 | break 20 | case 'mouseMoved': 21 | isClick = false 22 | triggerTouchEvent('touchmove', el, x, y) 23 | break 24 | case 'mouseReleased': 25 | triggerTouchEvent('touchend', el, x, y) 26 | if (isClick) { 27 | triggerMouseEvent('click', el, x, y) 28 | } 29 | isClick = false 30 | break 31 | case 'mouseWheel': 32 | if (!isUndef(deltaX) && !isUndef(deltaY)) { 33 | triggerScroll(el, deltaX, deltaY) 34 | } 35 | break 36 | } 37 | } 38 | 39 | export function dispatchMouseEvent(params: Input.DispatchMouseEventRequest) { 40 | const { type, x, y, deltaX, deltaY } = params 41 | 42 | const el = document.elementFromPoint(x, y) || document.documentElement 43 | 44 | switch (type) { 45 | case 'mousePressed': 46 | isClick = true 47 | triggerMouseEvent('mousedown', el, x, y) 48 | break 49 | case 'mouseMoved': 50 | isClick = false 51 | triggerMouseEvent('mousemove', el, x, y) 52 | break 53 | case 'mouseReleased': 54 | triggerMouseEvent('mouseup', el, x, y) 55 | if (isClick) { 56 | triggerMouseEvent('click', el, x, y) 57 | } 58 | isClick = false 59 | break 60 | case 'mouseWheel': 61 | if (!isUndef(deltaX) && !isUndef(deltaY)) { 62 | triggerScroll(el, deltaX, deltaY) 63 | } 64 | break 65 | } 66 | } 67 | 68 | function triggerMouseEvent(type: string, el: Element, x: number, y: number) { 69 | el.dispatchEvent( 70 | new MouseEvent(type, { 71 | bubbles: true, 72 | cancelable: true, 73 | view: window, 74 | clientX: x, 75 | clientY: y, 76 | }) 77 | ) 78 | } 79 | 80 | function triggerTouchEvent(type: string, el: Element, x: number, y: number) { 81 | const touch = new Touch({ 82 | identifier: 0, 83 | target: el, 84 | clientX: x, 85 | clientY: y, 86 | force: 1, 87 | }) 88 | 89 | el.dispatchEvent( 90 | new TouchEvent(type, { 91 | bubbles: true, 92 | touches: [touch], 93 | changedTouches: [touch], 94 | targetTouches: [touch], 95 | }) 96 | ) 97 | } 98 | 99 | function triggerScroll(el: Element, deltaX: number, deltaY: number) { 100 | el = findScrollableEl(el, deltaX, deltaY) 101 | el.scrollLeft -= deltaX 102 | el.scrollTop -= deltaY 103 | } 104 | 105 | function findScrollableEl(el: Element | null, deltaX: number, deltaY: number) { 106 | while (el) { 107 | if (toBool(deltaX) && isScrollable(el, 'x')) { 108 | return el 109 | } 110 | if (toBool(deltaY) && isScrollable(el, 'y')) { 111 | return el 112 | } 113 | 114 | el = el.parentElement 115 | } 116 | 117 | return el || document.documentElement 118 | } 119 | 120 | function isScrollable(el: Element, direction: 'x' | 'y') { 121 | const computedStyle = getComputedStyle(el) 122 | 123 | if (direction === 'x') { 124 | return ( 125 | el.scrollWidth > el.clientWidth && computedStyle.overflowX !== 'hidden' 126 | ) 127 | } 128 | 129 | return ( 130 | el.scrollHeight > el.clientHeight && computedStyle.overflowY !== 'hidden' 131 | ) 132 | } 133 | -------------------------------------------------------------------------------- /src/domains/Network.ts: -------------------------------------------------------------------------------- 1 | import trim from 'licia/trim' 2 | import each from 'licia/each' 3 | import decodeUriComponent from 'licia/decodeUriComponent' 4 | import rmCookie from 'licia/rmCookie' 5 | import isNative from 'licia/isNative' 6 | import contain from 'licia/contain' 7 | import now from 'licia/now' 8 | import Emitter from 'licia/Emitter' 9 | import isStr from 'licia/isStr' 10 | import isBlob from 'licia/isBlob' 11 | import isUndef from 'licia/isUndef' 12 | import types from 'licia/types' 13 | import convertBin from 'licia/convertBin' 14 | import { XhrRequest, FetchRequest } from '../lib/request' 15 | import connector from '../lib/connector' 16 | import { createId } from '../lib/util' 17 | import Protocol from 'devtools-protocol' 18 | import Network = Protocol.Network 19 | 20 | export function deleteCookies(params: Network.DeleteCookiesRequest) { 21 | rmCookie(params.name) 22 | } 23 | 24 | export function getCookies(): Network.GetCookiesResponse { 25 | const cookies: any[] = [] 26 | 27 | const cookie = document.cookie 28 | if (trim(cookie) !== '') { 29 | each(cookie.split(';'), function (value: any) { 30 | value = value.split('=') 31 | const name = trim(value.shift()) 32 | value = decodeUriComponent(value.join('=')) 33 | cookies.push({ 34 | name, 35 | value, 36 | }) 37 | }) 38 | } 39 | 40 | return { cookies } 41 | } 42 | 43 | const resTxtMap = new Map() 44 | 45 | let isEnable = false 46 | 47 | export const enable = function () { 48 | isEnable = true 49 | each(triggers, trigger => trigger()) 50 | triggers = [] 51 | } 52 | 53 | export function getResponseBody( 54 | params: Network.GetResponseBodyRequest 55 | ): Network.GetResponseBodyResponse { 56 | return { 57 | base64Encoded: false, 58 | body: resTxtMap.get(params.requestId), 59 | } 60 | } 61 | 62 | function enableXhr() { 63 | const winXhrProto = window.XMLHttpRequest.prototype 64 | 65 | const origSend: any = winXhrProto.send 66 | const origOpen: any = winXhrProto.open 67 | const origSetRequestHeader: any = winXhrProto.setRequestHeader 68 | 69 | winXhrProto.open = function (method: string, url: string) { 70 | if (!isValidUrl(url)) { 71 | return origOpen.apply(this, arguments) 72 | } 73 | 74 | const xhr = this 75 | 76 | const req = ((xhr as any).chobitsuRequest = new XhrRequest( 77 | xhr, 78 | method, 79 | url 80 | )) 81 | 82 | bindRequestEvent(req, 'XHR') 83 | 84 | origOpen.apply(this, arguments) 85 | } 86 | 87 | winXhrProto.send = function (data) { 88 | const req = (this as any).chobitsuRequest 89 | if (req) req.handleSend(data) 90 | 91 | origSend.apply(this, arguments) 92 | } 93 | 94 | winXhrProto.setRequestHeader = function (key, val) { 95 | const req = (this as any).chobitsuRequest 96 | if (req) { 97 | req.handleReqHeadersSet(key, val) 98 | } 99 | 100 | origSetRequestHeader.apply(this, arguments) 101 | } 102 | } 103 | 104 | function enableFetch() { 105 | let isFetchSupported = false 106 | if (window.fetch) { 107 | isFetchSupported = isNative(window.fetch) 108 | // #2 Probably not a fetch polyfill 109 | if (!isFetchSupported) { 110 | if (navigator.serviceWorker) { 111 | isFetchSupported = true 112 | } 113 | if (window.Request && isNative(window.Request)) { 114 | isFetchSupported = true 115 | } 116 | } 117 | } 118 | if (!isFetchSupported) return 119 | 120 | const origFetch = window.fetch 121 | 122 | window.fetch = function (...args) { 123 | const req = new FetchRequest(...args) 124 | bindRequestEvent(req, 'Fetch') 125 | const fetchResult = origFetch(...args) 126 | req.send(fetchResult) 127 | 128 | return fetchResult 129 | } 130 | } 131 | 132 | function bindRequestEvent(req: Emitter, type: string) { 133 | req.on('send', (id: string, data: any) => { 134 | const request: any = { 135 | method: data.method, 136 | url: data.url, 137 | headers: data.reqHeaders, 138 | } 139 | if (data.data) { 140 | request.postData = data.data 141 | } 142 | 143 | trigger('Network.requestWillBeSent', { 144 | requestId: id, 145 | type, 146 | request, 147 | timestamp: data.time / 1000, 148 | }) 149 | }) 150 | req.on('headersReceived', (id: string, data: any) => { 151 | trigger('Network.responseReceivedExtraInfo', { 152 | requestId: id, 153 | blockedCookies: [], 154 | headers: data.resHeaders, 155 | }) 156 | }) 157 | req.on('done', (id: string, data: any) => { 158 | const response: any = { 159 | status: data.status, 160 | } 161 | if (data.resHeaders) { 162 | response.headers = data.resHeaders 163 | } 164 | 165 | trigger('Network.responseReceived', { 166 | requestId: id, 167 | type, 168 | response, 169 | timestamp: data.time / 1000, 170 | }) 171 | resTxtMap.set(id, data.resTxt) 172 | trigger('Network.loadingFinished', { 173 | requestId: id, 174 | encodedDataLength: data.size, 175 | timestamp: data.time / 1000, 176 | }) 177 | }) 178 | req.on('error', (id, data) => { 179 | trigger('Network.loadingFailed', { 180 | requestId: id, 181 | errorText: data.errorText, 182 | timestamp: data.time / 1000, 183 | type, 184 | }) 185 | }) 186 | } 187 | 188 | function enableWebSocket() { 189 | const origWebSocket = window.WebSocket 190 | function WebSocket(url: string, protocols?: string | string[]) { 191 | const ws = new origWebSocket(url, protocols) 192 | 193 | if (!isValidUrl(url)) { 194 | return ws 195 | } 196 | 197 | const requestId = createId() 198 | 199 | trigger('Network.webSocketCreated', { 200 | requestId, 201 | url, 202 | }) 203 | 204 | ws.addEventListener('open', function () { 205 | trigger('Network.webSocketWillSendHandshakeRequest', { 206 | requestId, 207 | timestamp: now() / 1000, 208 | request: { 209 | headers: {}, 210 | }, 211 | }) 212 | trigger('Network.webSocketHandshakeResponseReceived', { 213 | requestId, 214 | timeStamp: now() / 1000, 215 | response: { 216 | status: 101, 217 | statusText: 'Switching Protocols', 218 | }, 219 | }) 220 | }) 221 | 222 | ws.addEventListener('message', async function (e) { 223 | let payloadData = e.data 224 | if (isUndef(payloadData)) { 225 | return 226 | } 227 | 228 | let opcode = 1 229 | if (!isStr(payloadData)) { 230 | opcode = 2 231 | if (isBlob(payloadData)) { 232 | payloadData = await convertBin.blobToArrBuffer(payloadData) 233 | } 234 | payloadData = convertBin(payloadData, 'base64') 235 | } 236 | 237 | trigger('Network.webSocketFrameReceived', { 238 | requestId, 239 | timestamp: now() / 1000, 240 | response: { 241 | opcode, 242 | payloadData, 243 | }, 244 | }) 245 | }) 246 | 247 | const origSend = ws.send 248 | ws.send = function (data: any) { 249 | if (!isUndef(data)) { 250 | frameSent(data) 251 | } 252 | 253 | return origSend.call(this, data) 254 | } 255 | 256 | async function frameSent(data: any) { 257 | let opcode = 1 258 | let payloadData = data 259 | if (!isStr(data)) { 260 | opcode = 2 261 | if (isBlob(payloadData)) { 262 | payloadData = await convertBin.blobToArrBuffer(payloadData) 263 | } 264 | payloadData = convertBin(data, 'base64') 265 | } 266 | 267 | trigger('Network.webSocketFrameSent', { 268 | requestId, 269 | timestamp: now() / 1000, 270 | response: { 271 | opcode, 272 | payloadData, 273 | }, 274 | }) 275 | } 276 | 277 | ws.addEventListener('close', function () { 278 | trigger('Network.webSocketClosed', { 279 | requestId, 280 | timestamp: now() / 1000, 281 | }) 282 | }) 283 | 284 | ws.addEventListener('error', function () { 285 | trigger('Network.webSocketFrameError', { 286 | requestId, 287 | timestamp: now() / 1000, 288 | errorMessage: 'WebSocket error', 289 | }) 290 | }) 291 | 292 | return ws 293 | } 294 | WebSocket.prototype = origWebSocket.prototype 295 | WebSocket.CLOSED = origWebSocket.CLOSED 296 | WebSocket.CLOSING = origWebSocket.CLOSING 297 | WebSocket.CONNECTING = origWebSocket.CONNECTING 298 | WebSocket.OPEN = origWebSocket.OPEN 299 | window.WebSocket = WebSocket as any 300 | } 301 | 302 | function isValidUrl(url: string) { 303 | return !contain(url, '__chobitsu-hide__=true') 304 | } 305 | 306 | let triggers: types.AnyFn[] = [] 307 | 308 | function trigger(method: string, params: any) { 309 | if (isEnable) { 310 | connector.trigger(method, params) 311 | } else { 312 | triggers.push(() => connector.trigger(method, params)) 313 | } 314 | } 315 | 316 | enableXhr() 317 | enableFetch() 318 | enableWebSocket() 319 | -------------------------------------------------------------------------------- /src/domains/Overlay.ts: -------------------------------------------------------------------------------- 1 | import { getNode, getNodeId, isValidNode } from '../lib/nodeManager' 2 | import { pushNodesToFrontend } from './DOM' 3 | import $ from 'licia/$' 4 | import h from 'licia/h' 5 | import evalCss from 'licia/evalCss' 6 | import defaults from 'licia/defaults' 7 | import extend from 'licia/extend' 8 | import connector from '../lib/connector' 9 | import root from 'licia/root' 10 | import toBool from 'licia/toBool' 11 | import cssSupports from 'licia/cssSupports' 12 | import LunaDomHighlighter from 'luna-dom-highlighter' 13 | import * as objManager from '../lib/objManager' 14 | import Protocol from 'devtools-protocol' 15 | import Overlay = Protocol.Overlay 16 | 17 | let domHighlighter: LunaDomHighlighter 18 | let isCssLoaded = false 19 | let $container: $.$ 20 | let isEnable = false 21 | const showInfo = cssSupports( 22 | 'clip-path', 23 | 'polygon(50% 0px, 0px 100%, 100% 100%)' 24 | ) 25 | const hasTouchSupport = 'ontouchstart' in root 26 | 27 | const css = require('luna-dom-highlighter/luna-dom-highlighter.css').replace( 28 | '/*# sourceMappingURL=luna-dom-highlighter.css.map*/', 29 | '' 30 | ) 31 | 32 | export function enable() { 33 | if (isEnable) { 34 | return 35 | } 36 | 37 | const container = h('div', { 38 | class: '__chobitsu-hide__', 39 | style: { 40 | all: 'initial', 41 | }, 42 | }) 43 | $container = $(container) 44 | document.documentElement.appendChild(container) 45 | 46 | let domHighlighterContainer: HTMLDivElement | null = null 47 | let shadowRoot: ShadowRoot | null = null 48 | if (container.attachShadow) { 49 | shadowRoot = container.attachShadow({ mode: 'open' }) 50 | } else if ((container as any).createShadowRoot) { 51 | shadowRoot = (container as any).createShadowRoot() 52 | } 53 | if (shadowRoot) { 54 | const style = document.createElement('style') 55 | style.textContent = css 56 | style.type = 'text/css' 57 | shadowRoot.appendChild(style) 58 | domHighlighterContainer = document.createElement('div') 59 | shadowRoot.appendChild(domHighlighterContainer) 60 | } else { 61 | domHighlighterContainer = document.createElement('div') 62 | container.appendChild(domHighlighterContainer) 63 | if (!isCssLoaded) { 64 | evalCss(css) 65 | isCssLoaded = true 66 | } 67 | } 68 | 69 | domHighlighter = new LunaDomHighlighter(domHighlighterContainer, { 70 | monitorResize: toBool(root.ResizeObserver), 71 | showInfo, 72 | }) 73 | 74 | window.addEventListener('resize', resizeHandler) 75 | 76 | isEnable = true 77 | } 78 | 79 | export function disable() { 80 | domHighlighter.destroy() 81 | $container.remove() 82 | window.removeEventListener('resize', resizeHandler) 83 | 84 | isEnable = false 85 | } 86 | 87 | export function highlightNode(params: Overlay.HighlightNodeRequest) { 88 | const { nodeId, highlightConfig, objectId } = params 89 | 90 | let node: any 91 | if (nodeId) { 92 | node = getNode(nodeId) 93 | } 94 | if (objectId) { 95 | node = objManager.getObj(objectId) 96 | } 97 | 98 | if (node.nodeType !== 1 && node.nodeType !== 3) return 99 | 100 | defaults(highlightConfig, { 101 | contentColor: 'transparent', 102 | paddingColor: 'transparent', 103 | borderColor: 'transparent', 104 | marginColor: 'transparent', 105 | }) 106 | if (!showInfo) { 107 | extend(highlightConfig, { 108 | showInfo: false, 109 | }) 110 | } 111 | domHighlighter.highlight(node, highlightConfig as any) 112 | } 113 | 114 | export function hideHighlight() { 115 | domHighlighter.hide() 116 | } 117 | 118 | let showViewportSizeOnResize = false 119 | export function setShowViewportSizeOnResize( 120 | params: Overlay.SetShowViewportSizeOnResizeRequest 121 | ) { 122 | showViewportSizeOnResize = params.show 123 | } 124 | 125 | let highlightConfig: any = {} 126 | let inspectMode = 'none' 127 | export function setInspectMode(params: Overlay.SetInspectModeRequest) { 128 | highlightConfig = params.highlightConfig 129 | inspectMode = params.mode 130 | } 131 | 132 | function getElementFromPoint(e: any) { 133 | if (hasTouchSupport) { 134 | const touch = e.touches[0] || e.changedTouches[0] 135 | return document.elementFromPoint(touch.clientX, touch.clientY) 136 | } 137 | 138 | return document.elementFromPoint(e.clientX, e.clientY) 139 | } 140 | 141 | let lastNodeId = -1 142 | 143 | function moveListener(e: any) { 144 | if (inspectMode === 'none') return 145 | 146 | const node = getElementFromPoint(e) 147 | if (!node || !isValidNode(node)) { 148 | return 149 | } 150 | let nodeId = getNodeId(node) 151 | 152 | if (!nodeId) { 153 | nodeId = pushNodesToFrontend(node) 154 | } 155 | 156 | highlightNode({ 157 | nodeId, 158 | highlightConfig, 159 | }) 160 | if (nodeId !== lastNodeId) { 161 | connector.trigger('Overlay.nodeHighlightRequested', { 162 | nodeId, 163 | }) 164 | lastNodeId = nodeId 165 | } 166 | } 167 | 168 | function outListener() { 169 | if (inspectMode === 'none') return 170 | 171 | hideHighlight() 172 | } 173 | 174 | function clickListener(e: any) { 175 | if (inspectMode === 'none') return 176 | 177 | e.preventDefault() 178 | e.stopImmediatePropagation() 179 | 180 | const node = getElementFromPoint(e) 181 | connector.trigger('Overlay.inspectNodeRequested', { 182 | backendNodeId: getNodeId(node), 183 | }) 184 | 185 | lastNodeId = -1 186 | hideHighlight() 187 | } 188 | 189 | function addEvent(type: string, listener: any) { 190 | document.documentElement.addEventListener(type, listener, true) 191 | } 192 | if (hasTouchSupport) { 193 | addEvent('touchstart', moveListener) 194 | addEvent('touchmove', moveListener) 195 | addEvent('touchend', clickListener) 196 | } else { 197 | addEvent('mousemove', moveListener) 198 | addEvent('mouseout', outListener) 199 | addEvent('click', clickListener) 200 | } 201 | 202 | const viewportSize = h('div', { 203 | class: '__chobitsu-hide__', 204 | style: { 205 | position: 'fixed', 206 | right: 0, 207 | top: 0, 208 | background: '#fff', 209 | fontSize: 13, 210 | opacity: 0.5, 211 | padding: '4px 6px', 212 | }, 213 | }) 214 | 215 | function resizeHandler() { 216 | if (!showViewportSizeOnResize) return 217 | 218 | $viewportSize.text(`${window.innerWidth}px × ${window.innerHeight}px`) 219 | if (viewportSizeTimer) { 220 | clearTimeout(viewportSizeTimer) 221 | } else { 222 | document.documentElement.appendChild(viewportSize) 223 | } 224 | viewportSizeTimer = setTimeout(() => { 225 | $viewportSize.remove() 226 | viewportSizeTimer = null 227 | }, 1000) 228 | } 229 | const $viewportSize: any = $(viewportSize) 230 | let viewportSizeTimer: any 231 | -------------------------------------------------------------------------------- /src/domains/Page.ts: -------------------------------------------------------------------------------- 1 | import $ from 'licia/$' 2 | import contain from 'licia/contain' 3 | import fetch from 'licia/fetch' 4 | import now from 'licia/now' 5 | import Readiness from 'licia/Readiness' 6 | import map from 'licia/map' 7 | import { fullUrl } from '../lib/request' 8 | import { MAIN_FRAME_ID } from '../lib/constants' 9 | import { 10 | getBase64Content, 11 | getTextContent, 12 | getOrigin, 13 | getUrl, 14 | } from '../lib/util' 15 | import connector from '../lib/connector' 16 | import { isValidNode } from '../lib/nodeManager' 17 | import html2canvas, { Options as html2canvasOptions } from 'html2canvas' 18 | import * as resources from '../lib/resources' 19 | import Protocol from 'devtools-protocol' 20 | import Page = Protocol.Page 21 | 22 | let proxy = '' 23 | 24 | export function setProxy(params: any) { 25 | proxy = params.proxy 26 | } 27 | 28 | export function enable() { 29 | stopScreencast() 30 | } 31 | 32 | export function reload() { 33 | location.reload() 34 | } 35 | 36 | export function navigate(params: Page.NavigateRequest): Page.NavigateResponse { 37 | location.href = params.url 38 | 39 | return { 40 | frameId: MAIN_FRAME_ID, 41 | } 42 | } 43 | 44 | export function getNavigationHistory() { 45 | return { 46 | currentIndex: 0, 47 | entries: [ 48 | { 49 | id: 0, 50 | url: getUrl(), 51 | userTypedURL: getUrl(), 52 | title: document.title, 53 | transitionType: 'link', 54 | }, 55 | ], 56 | } 57 | } 58 | 59 | export async function getAppManifest() { 60 | const $links = $('link') 61 | const ret: any = { 62 | errors: [], 63 | } 64 | 65 | let url = '' 66 | $links.each(function (this: Element) { 67 | const $this = $(this) 68 | 69 | if ($this.attr('rel') === 'manifest') { 70 | url = fullUrl($this.attr('href')) 71 | } 72 | }) 73 | ret.url = url 74 | 75 | if (url) { 76 | const res = await fetch(url) 77 | ret.data = await res.text() 78 | } 79 | 80 | return ret 81 | } 82 | 83 | export function getResourceTree() { 84 | const images = map(resources.getImages(), url => { 85 | let mimeType = 'image/jpg' 86 | if (contain(url, 'png')) { 87 | mimeType = 'image/png' 88 | } else if (contain(url, 'gif')) { 89 | mimeType = 'image/gif' 90 | } 91 | 92 | return { 93 | url, 94 | mimeType, 95 | type: 'Image', 96 | } 97 | }) 98 | 99 | return { 100 | frameTree: { 101 | frame: { 102 | id: MAIN_FRAME_ID, 103 | mimeType: 'text/html', 104 | securityOrigin: getOrigin(), 105 | url: getUrl(), 106 | }, 107 | resources: [...images], 108 | }, 109 | } 110 | } 111 | 112 | export async function getResourceContent( 113 | params: Page.GetResourceContentRequest 114 | ): Promise { 115 | const { frameId, url } = params 116 | let base64Encoded = false 117 | 118 | if (frameId === MAIN_FRAME_ID) { 119 | let content = '' 120 | 121 | if (url === location.href) { 122 | content = await getTextContent(url) 123 | if (!content) { 124 | content = document.documentElement.outerHTML 125 | } 126 | } else if (resources.isImage(url)) { 127 | content = await getBase64Content(url, proxy) 128 | base64Encoded = true 129 | } 130 | 131 | return { 132 | base64Encoded, 133 | content, 134 | } 135 | } 136 | 137 | return { 138 | base64Encoded, 139 | content: '', 140 | } 141 | } 142 | 143 | let screenshotTimer: any 144 | let ack: Readiness 145 | let screencastInterval = 2000 146 | let isCapturingScreenshot = false 147 | 148 | export function screencastFrameAck() { 149 | if (ack) { 150 | ack.signal('ack') 151 | } 152 | } 153 | 154 | export function startScreencast() { 155 | if (isCapturingScreenshot) { 156 | return 157 | } 158 | stopScreencast() 159 | captureScreenshot() 160 | } 161 | 162 | export function stopScreencast() { 163 | if (screenshotTimer) { 164 | clearTimeout(screenshotTimer) 165 | screenshotTimer = null 166 | if (ack) { 167 | ack.signal('ack') 168 | } 169 | } 170 | } 171 | 172 | async function captureScreenshot() { 173 | if (document.hidden) { 174 | screenshotTimer = setTimeout(captureScreenshot, screencastInterval) 175 | return 176 | } 177 | 178 | isCapturingScreenshot = true 179 | 180 | const $body = $(document.body) 181 | const deviceWidth = window.innerWidth 182 | let deviceHeight = window.innerHeight 183 | let offsetTop = -window.scrollY 184 | const overflowY = $body.css('overflow-y') 185 | if (contain(['auto', 'scroll'], overflowY)) { 186 | deviceHeight = $body.offset().height 187 | offsetTop = -document.body.scrollTop 188 | } 189 | let width = $body.offset().width 190 | if (width < deviceWidth) { 191 | width = deviceWidth 192 | } 193 | 194 | const options: Partial = { 195 | imageTimeout: 5000, 196 | scale: 1, 197 | width, 198 | logging: false, 199 | ignoreElements(node) { 200 | return !isValidNode(node) 201 | }, 202 | } 203 | 204 | if (proxy) { 205 | options.proxy = proxy 206 | } else { 207 | options.foreignObjectRendering = true 208 | options.useCORS = true 209 | } 210 | 211 | const time = now() 212 | const canvas = await html2canvas(document.body, options) 213 | const duration = now() - time 214 | screencastInterval = 2000 215 | if (duration * 5 > screencastInterval) { 216 | screencastInterval = duration * 5 217 | } 218 | 219 | const data = canvas 220 | .toDataURL('image/jpeg') 221 | .replace(/^data:image\/jpeg;base64,/, '') 222 | 223 | if (ack) { 224 | await ack.ready('ack') 225 | } 226 | ack = new Readiness() 227 | connector.trigger('Page.screencastFrame', { 228 | data, 229 | sessionId: 1, 230 | metadata: { 231 | deviceWidth, 232 | deviceHeight, 233 | pageScaleFactor: 1, 234 | offsetTop, 235 | scrollOffsetX: 0, 236 | scrollOffsetY: 0, 237 | timestamp: now(), 238 | }, 239 | }) 240 | 241 | screenshotTimer = setTimeout(captureScreenshot, screencastInterval) 242 | 243 | isCapturingScreenshot = false 244 | } 245 | -------------------------------------------------------------------------------- /src/domains/Runtime.ts: -------------------------------------------------------------------------------- 1 | import connector from '../lib/connector' 2 | import each from 'licia/each' 3 | import map from 'licia/map' 4 | import now from 'licia/now' 5 | import isStr from 'licia/isStr' 6 | import fnParams from 'licia/fnParams' 7 | import uncaught from 'licia/uncaught' 8 | import startWith from 'licia/startWith' 9 | import stackTrace from 'licia/stackTrace' 10 | import trim from 'licia/trim' 11 | import types from 'licia/types' 12 | import * as objManager from '../lib/objManager' 13 | import evaluateJs, { setGlobal } from '../lib/evaluate' 14 | import Protocol from 'devtools-protocol' 15 | import Runtime = Protocol.Runtime 16 | 17 | const executionContext = { 18 | id: 1, 19 | name: 'top', 20 | origin: location.origin, 21 | } 22 | 23 | export async function callFunctionOn( 24 | params: Runtime.CallFunctionOnRequest 25 | ): Promise { 26 | const { functionDeclaration, objectId } = params 27 | let args = params.arguments || [] 28 | 29 | args = map(args, (arg: any) => { 30 | const { objectId, value } = arg 31 | if (objectId) { 32 | const obj = objManager.getObj(objectId) 33 | if (obj) return obj 34 | } 35 | 36 | return value 37 | }) 38 | 39 | let ctx = null 40 | if (objectId) { 41 | ctx = objManager.getObj(objectId) 42 | } 43 | 44 | return { 45 | result: objManager.wrap(await callFn(functionDeclaration, args, ctx)), 46 | } 47 | } 48 | 49 | let isEnable = false 50 | 51 | export function enable() { 52 | isEnable = true 53 | each(triggers, trigger => trigger()) 54 | triggers = [] 55 | 56 | trigger('Runtime.executionContextCreated', { 57 | context: executionContext, 58 | }) 59 | } 60 | 61 | export function getProperties( 62 | params: Runtime.GetPropertiesRequest 63 | ): Runtime.GetPropertiesResponse { 64 | return objManager.getProperties(params) 65 | } 66 | 67 | export function evaluate( 68 | params: Runtime.EvaluateRequest 69 | ): Runtime.EvaluateResponse { 70 | const ret: any = {} 71 | 72 | let result: any 73 | try { 74 | if (params.throwOnSideEffect && hasSideEffect(params.expression)) { 75 | throw EvalError('Possible side-effect in debug-evaluate') 76 | } 77 | result = evaluateJs(params.expression) 78 | setGlobal('$_', result) 79 | ret.result = objManager.wrap(result, { 80 | generatePreview: true, 81 | }) 82 | } catch (e) { 83 | ret.exceptionDetails = { 84 | exception: objManager.wrap(e), 85 | text: 'Uncaught', 86 | } 87 | ret.result = objManager.wrap(e, { 88 | generatePreview: true, 89 | }) 90 | } 91 | 92 | return ret 93 | } 94 | 95 | export function releaseObject(params: Runtime.ReleaseObjectRequest) { 96 | objManager.releaseObj(params.objectId) 97 | } 98 | 99 | export function globalLexicalScopeNames() { 100 | return { 101 | names: [], 102 | } 103 | } 104 | 105 | declare const console: any 106 | 107 | function monitorConsole() { 108 | const methods: any = { 109 | log: 'log', 110 | warn: 'warning', 111 | error: 'error', 112 | info: 'info', 113 | dir: 'dir', 114 | table: 'table', 115 | group: 'startGroup', 116 | groupCollapsed: 'startGroupCollapsed', 117 | groupEnd: 'endGroup', 118 | debug: 'debug', 119 | clear: 'clear', 120 | } 121 | 122 | each(methods, (type, name) => { 123 | if (!console[name]) { 124 | return 125 | } 126 | const origin = console[name].bind(console) 127 | console[name] = (...args: any[]) => { 128 | origin(...args) 129 | 130 | args = map(args, arg => 131 | objManager.wrap(arg, { 132 | generatePreview: true, 133 | }) 134 | ) 135 | 136 | trigger('Runtime.consoleAPICalled', { 137 | type, 138 | args, 139 | stackTrace: { 140 | callFrames: 141 | type === 'error' || type === 'warning' ? getCallFrames() : [], 142 | }, 143 | executionContextId: executionContext.id, 144 | timestamp: now(), 145 | }) 146 | } 147 | }) 148 | } 149 | 150 | const Function = window.Function 151 | /* eslint-disable-next-line */ 152 | const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor 153 | 154 | function parseFn(fnStr: string) { 155 | const result = fnParams(fnStr) 156 | 157 | if (fnStr[fnStr.length - 1] !== '}') { 158 | result.push('return ' + fnStr.slice(fnStr.indexOf('=>') + 2)) 159 | } else { 160 | result.push(fnStr.slice(fnStr.indexOf('{') + 1, fnStr.lastIndexOf('}'))) 161 | } 162 | 163 | return result 164 | } 165 | 166 | async function callFn( 167 | functionDeclaration: string, 168 | args: any[], 169 | ctx: any = null 170 | ) { 171 | const fnParams = parseFn(functionDeclaration) 172 | let fn 173 | 174 | if (startWith(functionDeclaration, 'async')) { 175 | fn = AsyncFunction.apply(null, fnParams) 176 | return await fn.apply(ctx, args) 177 | } 178 | 179 | fn = Function.apply(null, fnParams) 180 | return fn.apply(ctx, args) 181 | } 182 | 183 | uncaught.addListener(err => { 184 | trigger('Runtime.exceptionThrown', { 185 | exceptionDetails: { 186 | exception: objManager.wrap(err), 187 | stackTrace: { callFrames: getCallFrames(err) }, 188 | text: 'Uncaught', 189 | }, 190 | timestamp: now, 191 | }) 192 | }) 193 | 194 | function hasSideEffect(code: string) { 195 | return !/^[a-zA-Z0-9]*$/.test(code) 196 | } 197 | 198 | function getCallFrames(error?: Error) { 199 | let callFrames: any[] = [] 200 | const callSites: any = error ? error.stack : stackTrace() 201 | if (isStr(callSites)) { 202 | callFrames = callSites.split('\n') 203 | if (!error) { 204 | callFrames.shift() 205 | } 206 | callFrames.shift() 207 | callFrames = map(callFrames, val => ({ functionName: trim(val) })) 208 | } else if (callSites) { 209 | callSites.shift() 210 | callFrames = map(callSites, (callSite: any) => { 211 | return { 212 | functionName: callSite.getFunctionName(), 213 | lineNumber: callSite.getLineNumber(), 214 | columnNumber: callSite.getColumnNumber(), 215 | url: callSite.getFileName(), 216 | } 217 | }) 218 | } 219 | return callFrames 220 | } 221 | 222 | let triggers: types.AnyFn[] = [] 223 | 224 | function trigger(method: string, params: any) { 225 | if (isEnable) { 226 | connector.trigger(method, params) 227 | } else { 228 | triggers.push(() => connector.trigger(method, params)) 229 | } 230 | } 231 | 232 | uncaught.start() 233 | monitorConsole() 234 | -------------------------------------------------------------------------------- /src/domains/Storage.ts: -------------------------------------------------------------------------------- 1 | import each from 'licia/each' 2 | import rmCookie from 'licia/rmCookie' 3 | import safeStorage from 'licia/safeStorage' 4 | import connector from '../lib/connector' 5 | import { getCookies } from './Network' 6 | import Protocol from 'devtools-protocol' 7 | import Storage = Protocol.Storage 8 | 9 | const localStore = safeStorage('local') 10 | const sessionStore = safeStorage('session') 11 | 12 | export function getUsageAndQuota(): Storage.GetUsageAndQuotaResponse { 13 | return { 14 | quota: 0, 15 | usage: 0, 16 | overrideActive: false, 17 | usageBreakdown: [], 18 | } 19 | } 20 | 21 | export function clearDataForOrigin(params: Storage.ClearDataForOriginRequest) { 22 | const storageTypes = params.storageTypes.split(',') 23 | 24 | each(storageTypes, type => { 25 | if (type === 'cookies') { 26 | const cookies = getCookies().cookies 27 | each(cookies, ({ name }) => rmCookie(name)) 28 | } else if (type === 'local_storage') { 29 | localStore.clear() 30 | sessionStore.clear() 31 | } 32 | }) 33 | } 34 | 35 | export function getTrustTokens(): Storage.GetTrustTokensResponse { 36 | return { 37 | tokens: [], 38 | } 39 | } 40 | 41 | export function getStorageKeyForFrame(): Storage.GetStorageKeyForFrameResponse { 42 | return { 43 | storageKey: location.origin, 44 | } 45 | } 46 | 47 | export function getSharedStorageMetadata(): Storage.GetSharedStorageMetadataResponse { 48 | return { 49 | metadata: { 50 | creationTime: 0, 51 | length: 0, 52 | remainingBudget: 0, 53 | bytesUsed: 0, 54 | }, 55 | } 56 | } 57 | 58 | export function setStorageBucketTracking() { 59 | connector.trigger('Storage.storageBucketCreatedOrUpdated', { 60 | bucketInfo: { 61 | bucket: { 62 | storageKey: location.origin, 63 | }, 64 | durability: 'relaxed', 65 | expiration: 0, 66 | id: '0', 67 | persistent: false, 68 | quota: 0, 69 | }, 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare const chobitsu: { 2 | domain(name: string): { [index: string]: (...args: any[]) => any } 3 | sendRawMessage(message: string): void 4 | sendMessage(method: string, params?: any): Promise 5 | setOnMessage(onMessage: (message: string) => void): void 6 | } 7 | 8 | export = chobitsu 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/stable/object/assign' 2 | import 'core-js/stable/promise' 3 | import 'core-js/modules/es.map' 4 | import Chobitsu from './Chobitsu' 5 | import noop from 'licia/noop' 6 | import * as Runtime from './domains/Runtime' 7 | import * as Page from './domains/Page' 8 | import * as DOM from './domains/DOM' 9 | import * as CSS from './domains/CSS' 10 | import * as DOMStorage from './domains/DOMStorage' 11 | import * as Network from './domains/Network' 12 | import * as Overlay from './domains/Overlay' 13 | import * as DOMDebugger from './domains/DOMDebugger' 14 | import * as Debugger from './domains/Debugger' 15 | import * as Storage from './domains/Storage' 16 | import * as CacheStorage from './domains/CacheStorage' 17 | import * as IndexedDB from './domains/IndexedDB' 18 | import * as Input from './domains/Input' 19 | 20 | const chobitsu = new Chobitsu() 21 | chobitsu.register('Network', { 22 | ...Network, 23 | setAttachDebugStack: noop, 24 | clearAcceptedEncodingsOverride: noop, 25 | }) 26 | chobitsu.register('Page', { 27 | ...Page, 28 | getManifestIcons: noop, 29 | bringToFront: noop, 30 | getInstallabilityErrors: noop, 31 | setAdBlockingEnabled: noop, 32 | getAppId: noop, 33 | }) 34 | chobitsu.register('Runtime', { 35 | ...Runtime, 36 | getExceptionDetails: noop, 37 | compileScript: noop, 38 | discardConsoleEntries: noop, 39 | getHeapUsage: noop, 40 | getIsolateId: noop, 41 | releaseObject: noop, 42 | releaseObjectGroup: noop, 43 | runIfWaitingForDebugger: noop, 44 | }) 45 | chobitsu.register('DOM', { 46 | ...DOM, 47 | pushNodeByPathToFrontend: noop, 48 | getNodeId: DOM.getDOMNodeId, 49 | getNode: DOM.getDOMNode, 50 | markUndoableState: noop, 51 | undo: noop, 52 | getBoxModel: noop, 53 | }) 54 | chobitsu.register('CSS', { 55 | ...CSS, 56 | getPlatformFontsForNode: noop, 57 | trackComputedStyleUpdates: noop, 58 | takeComputedStyleUpdates: noop, 59 | }) 60 | chobitsu.register('Debugger', { 61 | ...Debugger, 62 | getPossibleBreakpoints: noop, 63 | setBreakpointByUrl: noop, 64 | setBreakpointsActive: noop, 65 | setAsyncCallStackDepth: noop, 66 | setBlackboxPatterns: noop, 67 | setPauseOnExceptions: noop, 68 | }) 69 | chobitsu.register('Overlay', { 70 | ...Overlay, 71 | setPausedInDebuggerMessage: noop, 72 | highlightFrame: noop, 73 | setShowGridOverlays: noop, 74 | setShowFlexOverlays: noop, 75 | setShowScrollSnapOverlays: noop, 76 | setShowContainerQueryOverlays: noop, 77 | setShowIsolatedElements: noop, 78 | }) 79 | chobitsu.register('Profiler', { 80 | enable: noop, 81 | }) 82 | chobitsu.register('Log', { 83 | clear: noop, 84 | enable: noop, 85 | startViolationsReport: noop, 86 | }) 87 | chobitsu.register('Emulation', { 88 | setEmulatedMedia: noop, 89 | setAutoDarkModeOverride: noop, 90 | setEmulatedVisionDeficiency: noop, 91 | setFocusEmulationEnabled: noop, 92 | setTouchEmulationEnabled: noop, 93 | setEmitTouchEventsForMouse: noop, 94 | }) 95 | chobitsu.register('Audits', { 96 | enable: noop, 97 | }) 98 | chobitsu.register('ServiceWorker', { 99 | enable: noop, 100 | }) 101 | chobitsu.register('Inspector', { 102 | enable: noop, 103 | }) 104 | chobitsu.register('Target', { 105 | setAutoAttach: noop, 106 | setDiscoverTargets: noop, 107 | setRemoteLocations: noop, 108 | }) 109 | chobitsu.register('DOMDebugger', { 110 | ...DOMDebugger, 111 | setBreakOnCSPViolation: noop, 112 | }) 113 | chobitsu.register('Database', { 114 | enable: noop, 115 | }) 116 | chobitsu.register('CacheStorage', { 117 | ...CacheStorage, 118 | }) 119 | chobitsu.register('Storage', { 120 | ...Storage, 121 | setInterestGroupTracking: noop, 122 | setSharedStorageTracking: noop, 123 | trackIndexedDBForStorageKey: noop, 124 | untrackCacheStorageForOrigin: noop, 125 | untrackIndexedDBForOrigin: noop, 126 | trackCacheStorageForOrigin: noop, 127 | trackIndexedDBForOrigin: noop, 128 | }) 129 | chobitsu.register('DOMStorage', { 130 | ...DOMStorage, 131 | }) 132 | chobitsu.register('IndexedDB', { 133 | enable: noop, 134 | ...IndexedDB, 135 | }) 136 | chobitsu.register('ApplicationCache', { 137 | enable: noop, 138 | getFramesWithManifests: noop, 139 | }) 140 | chobitsu.register('BackgroundService', { 141 | startObserving: noop, 142 | }) 143 | chobitsu.register('HeapProfiler', { 144 | enable: noop, 145 | }) 146 | chobitsu.register('Input', { 147 | ...Input, 148 | }) 149 | chobitsu.register('Autofill', { 150 | enable: noop, 151 | }) 152 | 153 | export default chobitsu 154 | -------------------------------------------------------------------------------- /src/lib/connector.ts: -------------------------------------------------------------------------------- 1 | import Emitter from 'licia/Emitter' 2 | 3 | class Connector extends Emitter { 4 | trigger(method: string, params: any) { 5 | this.emit( 6 | 'message', 7 | JSON.stringify({ 8 | method, 9 | params, 10 | }) 11 | ) 12 | } 13 | } 14 | 15 | export default new Connector() 16 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const MAIN_FRAME_ID = '1' 2 | -------------------------------------------------------------------------------- /src/lib/evaluate.ts: -------------------------------------------------------------------------------- 1 | import isStr from 'licia/isStr' 2 | import copy from 'licia/copy' 3 | import toArr from 'licia/toArr' 4 | import keys from 'licia/keys' 5 | import xpath from 'licia/xpath' 6 | import each from 'licia/each' 7 | 8 | const global: any = { 9 | copy(value: any) { 10 | if (!isStr(value)) value = JSON.stringify(value, null, 2) 11 | copy(value) 12 | }, 13 | $(selector: string) { 14 | return document.querySelector(selector) 15 | }, 16 | $$(selector: string) { 17 | return toArr(document.querySelectorAll(selector)) 18 | }, 19 | $x(path: string) { 20 | return xpath(path) 21 | }, 22 | keys, 23 | } 24 | 25 | declare const window: any 26 | 27 | function injectGlobal() { 28 | each(global, (val, name) => { 29 | if (window[name]) return 30 | 31 | window[name] = val 32 | }) 33 | } 34 | 35 | function clearGlobal() { 36 | each(global, (val, name) => { 37 | if (window[name] && window[name] === val) { 38 | delete window[name] 39 | } 40 | }) 41 | } 42 | 43 | export function setGlobal(name: string, val: any) { 44 | global[name] = val 45 | } 46 | 47 | export default function evaluate(expression: string) { 48 | let ret 49 | 50 | injectGlobal() 51 | try { 52 | ret = eval.call(window, `(${expression})`) 53 | } catch (e) { 54 | ret = eval.call(window, expression) 55 | } 56 | clearGlobal() 57 | 58 | return ret 59 | } 60 | -------------------------------------------------------------------------------- /src/lib/mutationObserver.ts: -------------------------------------------------------------------------------- 1 | import Emitter from 'licia/Emitter' 2 | import each from 'licia/each' 3 | 4 | class Observer extends Emitter { 5 | private observer: MutationObserver 6 | constructor() { 7 | super() 8 | this.observer = new MutationObserver(mutations => { 9 | each(mutations, mutation => this.handleMutation(mutation)) 10 | }) 11 | } 12 | observe(node: Node) { 13 | this.observer.observe(node, { 14 | attributes: true, 15 | childList: true, 16 | characterData: true, 17 | subtree: true, 18 | }) 19 | } 20 | disconnect() { 21 | this.observer.disconnect() 22 | } 23 | private handleMutation(mutation: MutationRecord) { 24 | if (mutation.type === 'attributes') { 25 | this.emit('attributes', mutation.target, mutation.attributeName) 26 | } else if (mutation.type === 'childList') { 27 | this.emit( 28 | 'childList', 29 | mutation.target, 30 | mutation.addedNodes, 31 | mutation.removedNodes 32 | ) 33 | } else if (mutation.type === 'characterData') { 34 | this.emit('characterData', mutation.target) 35 | } 36 | } 37 | } 38 | 39 | export default new Observer() 40 | -------------------------------------------------------------------------------- /src/lib/nodeManager.ts: -------------------------------------------------------------------------------- 1 | import map from 'licia/map' 2 | import filter from 'licia/filter' 3 | import each from 'licia/each' 4 | import trim from 'licia/trim' 5 | import contain from 'licia/contain' 6 | import extend from 'licia/extend' 7 | import { createErr } from './util' 8 | 9 | const nodes = new Map() 10 | const nodeIds = new Map() 11 | let id = 1 12 | 13 | export function getOrCreateNodeId(node: any) { 14 | let nodeId = nodeIds.get(node) 15 | if (nodeId) return nodeId 16 | 17 | nodeId = id++ 18 | nodeIds.set(node, nodeId) 19 | nodes.set(nodeId, node) 20 | 21 | return nodeId 22 | } 23 | 24 | export function clear() { 25 | nodes.clear() 26 | nodeIds.clear() 27 | } 28 | 29 | export function getNodeId(node: any) { 30 | return nodeIds.get(node) 31 | } 32 | 33 | export function wrap(node: any, { depth = 1 } = {}) { 34 | const nodeId = getOrCreateNodeId(node) 35 | 36 | const ret: any = { 37 | nodeName: node.nodeName, 38 | nodeType: node.nodeType, 39 | localName: node.localName || '', 40 | nodeValue: node.nodeValue || '', 41 | nodeId, 42 | backendNodeId: nodeId, 43 | } 44 | 45 | if (node.parentNode) { 46 | ret.parentId = getOrCreateNodeId(node.parentNode) 47 | } 48 | 49 | if (node.nodeType === 10) { 50 | return extend(ret, { 51 | publicId: '', 52 | systemId: '', 53 | }) 54 | } 55 | 56 | if (node.attributes) { 57 | const attributes: string[] = [] 58 | each(node.attributes, ({ name, value }) => attributes.push(name, value)) 59 | ret.attributes = attributes 60 | } 61 | 62 | if (node.shadowRoot) { 63 | ret.shadowRoots = [wrap(node.shadowRoot, { depth: 1 })] 64 | } else if (node.chobitsuShadowRoot) { 65 | ret.shadowRoots = [wrap(node.chobitsuShadowRoot, { depth: 1 })] 66 | } 67 | if (isShadowRoot(node)) { 68 | ret.shadowRootType = node.mode || 'user-agent' 69 | } 70 | 71 | const childNodes = filterNodes(node.childNodes) 72 | ret.childNodeCount = childNodes.length 73 | const hasOneTextNode = 74 | ret.childNodeCount === 1 && childNodes[0].nodeType === 3 75 | if (depth > 0 || hasOneTextNode) { 76 | ret.children = getChildNodes(node, depth) 77 | } 78 | 79 | return ret 80 | } 81 | 82 | export function getChildNodes(node: any, depth: number) { 83 | const childNodes = filterNodes(node.childNodes) 84 | 85 | return map(childNodes, node => wrap(node, { depth: depth - 1 })) 86 | } 87 | 88 | export function getPreviousNode(node: any) { 89 | let previousNode = node.previousSibling 90 | if (!previousNode) return 91 | 92 | while (!isValidNode(previousNode) && previousNode.previousSibling) { 93 | previousNode = previousNode.previousSibling 94 | } 95 | if (previousNode && isValidNode(previousNode)) { 96 | return previousNode 97 | } 98 | } 99 | 100 | export function filterNodes(childNodes: T): T { 101 | return (filter as any)(childNodes, (node: any) => isValidNode(node)) 102 | } 103 | 104 | export function isValidNode(node: Node): boolean { 105 | if (node.nodeType === 1) { 106 | const className = (node as Element).getAttribute('class') || '' 107 | if ( 108 | contain(className, '__chobitsu-hide__') || 109 | contain(className, 'html2canvas-container') 110 | ) { 111 | return false 112 | } 113 | } 114 | 115 | const isValid = !(node.nodeType === 3 && trim(node.nodeValue || '') === '') 116 | if (isValid && node.parentNode) { 117 | return isValidNode(node.parentNode) 118 | } 119 | 120 | return isValid 121 | } 122 | 123 | export function getNode(nodeId: number) { 124 | const node = nodes.get(nodeId) 125 | 126 | if (!node || node.nodeType === 10 || node.nodeType === 11) { 127 | throw createErr(-32000, 'Could not find node with given id') 128 | } 129 | 130 | return node 131 | } 132 | 133 | function isShadowRoot(node: any) { 134 | if (window.ShadowRoot) { 135 | return node instanceof ShadowRoot 136 | } 137 | 138 | return false 139 | } 140 | -------------------------------------------------------------------------------- /src/lib/objManager.ts: -------------------------------------------------------------------------------- 1 | import toStr from 'licia/toStr' 2 | import isNull from 'licia/isNull' 3 | import isArr from 'licia/isArr' 4 | import isFn from 'licia/isFn' 5 | import isEl from 'licia/isEl' 6 | import isErr from 'licia/isErr' 7 | import isMap from 'licia/isMap' 8 | import isSet from 'licia/isSet' 9 | import isRegExp from 'licia/isRegExp' 10 | import getKeys from 'licia/keys' 11 | import toSrc from 'licia/toSrc' 12 | import allKeys from 'licia/allKeys' 13 | import isNative from 'licia/isNative' 14 | import getProto from 'licia/getProto' 15 | import isSymbol from 'licia/isSymbol' 16 | import { getType, has } from './util' 17 | 18 | const objects = new Map() 19 | const objectIds = new Map() 20 | const selfs = new Map() 21 | const entries = new Map() 22 | let id = 1 23 | 24 | function getOrCreateObjId(obj: any, self: any) { 25 | let objId = objectIds.get(obj) 26 | if (objId) return objId 27 | 28 | objId = JSON.stringify({ 29 | injectedScriptId: 0, 30 | id: id++, 31 | }) 32 | objectIds.set(obj, objId) 33 | objects.set(objId, obj) 34 | selfs.set(objId, self) 35 | 36 | return objId 37 | } 38 | 39 | export function clear() { 40 | objects.clear() 41 | objectIds.clear() 42 | selfs.clear() 43 | } 44 | 45 | export function wrap( 46 | value: any, 47 | { generatePreview = false, self = value } = {} 48 | ): any { 49 | const ret = basic(value) 50 | const { type, subtype } = ret 51 | 52 | if (type === 'undefined') { 53 | return ret 54 | } 55 | 56 | if (type === 'string' || type === 'boolean' || subtype === 'null') { 57 | ret.value = value 58 | return ret 59 | } 60 | 61 | ret.description = getDescription(value, self) 62 | if (type === 'number') { 63 | ret.value = value 64 | return ret 65 | } 66 | 67 | if (type === 'symbol') { 68 | ret.objectId = getOrCreateObjId(value, self) 69 | return ret 70 | } 71 | 72 | if (type === 'function') { 73 | ret.className = 'Function' 74 | } else if (subtype === 'array') { 75 | ret.className = 'Array' 76 | } else if (subtype === 'map') { 77 | ret.className = 'Map' 78 | } else if (subtype === 'set') { 79 | ret.className = 'Set' 80 | } else if (subtype === 'regexp') { 81 | ret.className = 'RegExp' 82 | } else if (subtype === 'error') { 83 | ret.className = value.name 84 | } else { 85 | ret.className = getType(value, false) 86 | } 87 | 88 | if (generatePreview) { 89 | ret.preview = getPreview(value, self) 90 | } 91 | 92 | ret.objectId = getOrCreateObjId(value, self) 93 | 94 | return ret 95 | } 96 | 97 | export function getObj(objectId: string) { 98 | return objects.get(objectId) 99 | } 100 | 101 | export function releaseObj(objectId: string) { 102 | const object = getObj(objectId) 103 | objectIds.delete(object) 104 | selfs.delete(objectId) 105 | objects.delete(objectId) 106 | } 107 | 108 | export function getProperties(params: any) { 109 | const { accessorPropertiesOnly, objectId, ownProperties, generatePreview } = 110 | params 111 | const properties = [] 112 | 113 | const options = { 114 | prototype: !ownProperties, 115 | unenumerable: true, 116 | symbol: !accessorPropertiesOnly, 117 | } 118 | 119 | const obj = objects.get(objectId) 120 | const self = selfs.get(objectId) 121 | const keys = allKeys(obj, options) 122 | const proto = getProto(obj) 123 | for (let i = 0, len = keys.length; i < len; i++) { 124 | const name = keys[i] 125 | let propVal 126 | try { 127 | propVal = self[name] 128 | } catch (e) { 129 | /* tslint:disable-next-line */ 130 | } 131 | 132 | const property: any = { 133 | name: toStr(name), 134 | isOwn: has(self, name), 135 | } 136 | 137 | let descriptor = Object.getOwnPropertyDescriptor(obj, name) 138 | if (!descriptor && proto) { 139 | descriptor = Object.getOwnPropertyDescriptor(proto, name) 140 | } 141 | if (descriptor) { 142 | if (accessorPropertiesOnly) { 143 | if (!descriptor.get && !descriptor.set) { 144 | continue 145 | } 146 | } 147 | property.configurable = descriptor.configurable 148 | property.enumerable = descriptor.enumerable 149 | property.writable = descriptor.writable 150 | if (descriptor.get) { 151 | property.get = wrap(descriptor.get) 152 | } 153 | if (descriptor.set) { 154 | property.set = wrap(descriptor.set) 155 | } 156 | } 157 | 158 | if (proto && has(proto, name) && property.enumerable) { 159 | property.isOwn = true 160 | } 161 | 162 | let accessValue = true 163 | if (!property.isOwn && property.get) accessValue = false 164 | if (accessValue) { 165 | if (isSymbol(name)) { 166 | property.symbol = wrap(name) 167 | property.value = { type: 'undefined' } 168 | } else { 169 | property.value = wrap(propVal, { 170 | generatePreview, 171 | }) 172 | } 173 | } 174 | 175 | if (accessorPropertiesOnly) { 176 | if (isFn(propVal) && isNative(propVal)) continue 177 | } 178 | 179 | properties.push(property) 180 | } 181 | if (proto && !ownProperties && !noPrototype(obj)) { 182 | properties.push({ 183 | name: '__proto__', 184 | configurable: true, 185 | enumerable: false, 186 | isOwn: has(obj, '__proto__'), 187 | value: wrap(proto, { 188 | self, 189 | }), 190 | writable: false, 191 | }) 192 | } 193 | 194 | if (accessorPropertiesOnly) { 195 | return { 196 | result: properties, 197 | } 198 | } 199 | 200 | const internalProperties = [] 201 | if (proto && !noPrototype(obj)) { 202 | internalProperties.push({ 203 | name: '[[Prototype]]', 204 | value: wrap(proto, { 205 | self, 206 | }), 207 | }) 208 | } 209 | if (isMap(obj) || isSet(obj)) { 210 | const internalEntries = createInternalEntries(obj) 211 | internalProperties.push({ 212 | name: '[[Entries]]', 213 | value: wrap(internalEntries), 214 | }) 215 | } 216 | 217 | return { 218 | internalProperties, 219 | result: properties, 220 | } 221 | } 222 | 223 | const MAX_PREVIEW_LEN = 5 224 | 225 | function getPreview(obj: any, self: any = obj) { 226 | const ret = basic(obj) 227 | ret.description = getDescription(obj, self) 228 | let overflow = false 229 | const properties = [] 230 | 231 | const keys = getKeys(obj) 232 | let len = keys.length 233 | if (len > MAX_PREVIEW_LEN) { 234 | len = MAX_PREVIEW_LEN 235 | overflow = true 236 | } 237 | 238 | for (let i = 0; i < len; i++) { 239 | const name = keys[i] 240 | 241 | properties.push(getPropertyPreview(name, self[name])) 242 | } 243 | ret.properties = properties 244 | 245 | if (isMap(obj)) { 246 | const entries = [] 247 | let i = 0 248 | const keys = obj.keys() 249 | let key = keys.next().value 250 | while (key) { 251 | if (i > MAX_PREVIEW_LEN) { 252 | overflow = true 253 | break 254 | } 255 | entries.push({ 256 | key: getPreview(key), 257 | value: getPreview(obj.get(key)), 258 | }) 259 | i++ 260 | key = keys.next().value 261 | } 262 | 263 | ret.entries = entries 264 | } else if (isSet(obj)) { 265 | const entries = [] 266 | let i = 0 267 | const keys = obj.keys() 268 | let key = keys.next().value 269 | while (key) { 270 | if (i > MAX_PREVIEW_LEN) { 271 | overflow = true 272 | break 273 | } 274 | entries.push({ 275 | value: getPreview(key), 276 | }) 277 | i++ 278 | key = keys.next().value 279 | } 280 | 281 | ret.entries = entries 282 | } 283 | 284 | ret.overflow = overflow 285 | return ret 286 | } 287 | 288 | function getPropertyPreview(name: string, propVal: any) { 289 | const property: any = basic(propVal) 290 | property.name = name 291 | const { subtype, type } = property 292 | 293 | let value 294 | if (type === 'object') { 295 | if (subtype === 'null') { 296 | value = 'null' 297 | } else if (subtype === 'array') { 298 | value = `Array(${propVal.length})` 299 | } else if (subtype === 'map') { 300 | value = `Map(${propVal.size})` 301 | } else if (subtype === 'set') { 302 | value = `Set(${propVal.size})` 303 | } else { 304 | value = getType(propVal, false) 305 | } 306 | } else { 307 | value = toStr(propVal) 308 | } 309 | 310 | property.value = value 311 | 312 | return property 313 | } 314 | 315 | function getDescription(obj: any, self: any = obj) { 316 | let description = '' 317 | const { type, subtype } = basic(obj) 318 | 319 | if (type === 'string') { 320 | description = obj 321 | } else if (type === 'number') { 322 | description = toStr(obj) 323 | } else if (type === 'symbol') { 324 | description = toStr(obj) 325 | } else if (type === 'function') { 326 | description = toSrc(obj) 327 | } else if (subtype === 'array') { 328 | description = `Array(${obj.length})` 329 | } else if (subtype === 'map') { 330 | description = `Map(${self.size})` 331 | } else if (subtype === 'set') { 332 | description = `Set(${self.size})` 333 | } else if (subtype === 'regexp') { 334 | description = toStr(obj) 335 | } else if (subtype === 'error') { 336 | description = obj.stack 337 | } else if (subtype === 'internal#entry') { 338 | if (obj.name) { 339 | description = `{"${toStr(obj.name)}" => "${toStr(obj.value)}"}` 340 | } else { 341 | description = `"${toStr(obj.value)}"` 342 | } 343 | } else { 344 | description = getType(obj, false) 345 | } 346 | 347 | return description 348 | } 349 | 350 | function basic(value: any): any { 351 | const type = typeof value 352 | let subtype = 'object' 353 | 354 | if (value instanceof InternalEntry) { 355 | subtype = 'internal#entry' 356 | } else if (isNull(value)) { 357 | subtype = 'null' 358 | } else if (isArr(value)) { 359 | subtype = 'array' 360 | } else if (isRegExp(value)) { 361 | subtype = 'regexp' 362 | } else if (isErr(value)) { 363 | subtype = 'error' 364 | } else if (isMap(value)) { 365 | subtype = 'map' 366 | } else if (isSet(value)) { 367 | subtype = 'set' 368 | } else { 369 | try { 370 | // Accessing nodeType may throw exception 371 | if (isEl(value)) { 372 | subtype = 'node' 373 | } 374 | } catch (e) { 375 | /* tslint:disable-next-line */ 376 | } 377 | } 378 | 379 | return { 380 | type, 381 | subtype, 382 | } 383 | } 384 | 385 | class InternalEntry { 386 | name: any 387 | value: any 388 | constructor(value: any, name?: any) { 389 | if (name) { 390 | this.name = name 391 | } 392 | this.value = value 393 | } 394 | } 395 | 396 | function noPrototype(obj: any) { 397 | if (obj instanceof InternalEntry) { 398 | return true 399 | } 400 | 401 | if (obj[0] && obj[0] instanceof InternalEntry) { 402 | return true 403 | } 404 | 405 | return false 406 | } 407 | 408 | function createInternalEntries(obj: any) { 409 | const entryId = entries.get(obj) 410 | const internalEntries: InternalEntry[] = entryId ? getObj(entryId) : [] 411 | const objEntries = obj.entries() 412 | let entry = objEntries.next().value 413 | while (entry) { 414 | if (isMap(obj)) { 415 | internalEntries.push(new InternalEntry(entry[1], entry[0])) 416 | } else { 417 | internalEntries.push(new InternalEntry(entry[1])) 418 | } 419 | entry = objEntries.next().value 420 | } 421 | return internalEntries 422 | } 423 | -------------------------------------------------------------------------------- /src/lib/request.ts: -------------------------------------------------------------------------------- 1 | import Emitter from 'licia/Emitter' 2 | import isStr from 'licia/isStr' 3 | import last from 'licia/last' 4 | import Url from 'licia/Url' 5 | import isEmpty from 'licia/isEmpty' 6 | import trim from 'licia/trim' 7 | import now from 'licia/now' 8 | import each from 'licia/each' 9 | import startWith from 'licia/startWith' 10 | import toNum from 'licia/toNum' 11 | import { createId } from './util' 12 | 13 | export class XhrRequest extends Emitter { 14 | private xhr: XMLHttpRequest 15 | private method: string 16 | private url: string 17 | private id: string 18 | private reqHeaders: any 19 | constructor(xhr: XMLHttpRequest, method: string, url: string) { 20 | super() 21 | 22 | this.xhr = xhr 23 | this.reqHeaders = {} 24 | this.method = method 25 | this.url = fullUrl(url) 26 | this.id = createId() 27 | 28 | xhr.addEventListener('readystatechange', () => { 29 | if (xhr.readyState === 2) { 30 | this.handleHeadersReceived() 31 | } else if (xhr.readyState === 4) { 32 | if (xhr.status === 0) { 33 | this.handleError() 34 | } else { 35 | this.handleDone() 36 | } 37 | } 38 | }) 39 | } 40 | // #1 41 | toJSON() { 42 | return { 43 | method: this.method, 44 | url: this.url, 45 | id: this.id, 46 | } 47 | } 48 | handleSend(data: any) { 49 | if (!isStr(data)) data = '' 50 | 51 | data = { 52 | name: getFileName(this.url), 53 | url: this.url, 54 | data, 55 | time: now(), 56 | reqHeaders: this.reqHeaders, 57 | method: this.method, 58 | } 59 | if (!isEmpty(this.reqHeaders)) { 60 | data.reqHeaders = this.reqHeaders 61 | } 62 | this.emit('send', this.id, data) 63 | } 64 | handleReqHeadersSet(key: string, val: string) { 65 | if (key && val) { 66 | this.reqHeaders[key] = val 67 | } 68 | } 69 | private handleHeadersReceived() { 70 | const { xhr } = this 71 | 72 | const type = getType(xhr.getResponseHeader('Content-Type') || '') 73 | this.emit('headersReceived', this.id, { 74 | type: type.type, 75 | subType: type.subType, 76 | size: getSize(xhr, true, this.url), 77 | time: now(), 78 | resHeaders: getHeaders(xhr), 79 | }) 80 | } 81 | private handleDone() { 82 | const xhr = this.xhr 83 | const resType = xhr.responseType 84 | let resTxt = '' 85 | 86 | const update = () => { 87 | this.emit('done', this.id, { 88 | status: xhr.status, 89 | size: getSize(xhr, false, this.url), 90 | time: now(), 91 | resTxt, 92 | }) 93 | } 94 | 95 | const type = getType(xhr.getResponseHeader('Content-Type') || '') 96 | if ( 97 | resType === 'blob' && 98 | (type.type === 'text' || 99 | type.subType === 'javascript' || 100 | type.subType === 'json') 101 | ) { 102 | readBlobAsText(xhr.response, (err: Error, result: string) => { 103 | if (result) resTxt = result 104 | update() 105 | }) 106 | } else { 107 | if (resType === '' || resType === 'text') resTxt = xhr.responseText 108 | if (resType === 'json') resTxt = JSON.stringify(xhr.response) 109 | 110 | update() 111 | } 112 | } 113 | private handleError() { 114 | this.emit('error', this.id, { 115 | errorText: 'Network error', 116 | time: now(), 117 | }) 118 | } 119 | } 120 | 121 | export class FetchRequest extends Emitter { 122 | private url: string 123 | private id: string 124 | private method: string 125 | private options: any 126 | private reqHeaders: any 127 | constructor(input: any, options: any = {}) { 128 | super() 129 | 130 | const isRequest = input instanceof window.Request 131 | const url = isRequest ? input.url : input 132 | 133 | this.url = fullUrl(url) 134 | this.id = createId() 135 | this.options = options 136 | this.reqHeaders = options.headers || (isRequest ? input.headers : {}) 137 | this.method = options.method || (isRequest ? input.method : 'GET') 138 | } 139 | send(fetchResult: any) { 140 | const options = this.options 141 | 142 | const data = isStr(options.body) ? options.body : '' 143 | 144 | this.emit('send', this.id, { 145 | name: getFileName(this.url), 146 | url: this.url, 147 | data, 148 | reqHeaders: this.reqHeaders, 149 | time: now(), 150 | method: this.method, 151 | }) 152 | 153 | fetchResult 154 | .then((res: any) => { 155 | res = res.clone() 156 | 157 | const type = getType(res.headers.get('Content-Type')) 158 | res.text().then((resTxt: string) => { 159 | const data: any = { 160 | type: type.type, 161 | subType: type.subType, 162 | time: now(), 163 | size: getFetchSize(res, resTxt), 164 | resTxt, 165 | resHeaders: getFetchHeaders(res), 166 | status: res.status, 167 | } 168 | if (!isEmpty(this.reqHeaders)) { 169 | data.reqHeaders = this.reqHeaders 170 | } 171 | this.emit('done', this.id, data) 172 | }) 173 | 174 | return res 175 | }) 176 | .catch((err: any) => { 177 | this.emit('error', this.id, { 178 | errorText: err.message, 179 | time: now(), 180 | }) 181 | }) 182 | } 183 | } 184 | 185 | function getFetchSize(res: any, resTxt: string) { 186 | let size = 0 187 | 188 | const contentLen = res.headers.get('Content-length') 189 | 190 | if (contentLen) { 191 | size = toNum(contentLen) 192 | } else { 193 | size = lenToUtf8Bytes(resTxt) 194 | } 195 | 196 | return size 197 | } 198 | 199 | function getFetchHeaders(res: any) { 200 | const ret: any = {} 201 | 202 | res.headers.forEach((val: string, key: string) => (ret[key] = val)) 203 | 204 | return ret 205 | } 206 | 207 | function getHeaders(xhr: XMLHttpRequest) { 208 | const raw = xhr.getAllResponseHeaders() 209 | const lines = raw.split('\n') 210 | 211 | const ret: any = {} 212 | 213 | each(lines, line => { 214 | line = trim(line) 215 | 216 | if (line === '') return 217 | 218 | const [key, val] = line.split(':', 2) 219 | 220 | ret[key] = trim(val) 221 | }) 222 | 223 | return ret 224 | } 225 | 226 | function getSize(xhr: XMLHttpRequest, headersOnly: boolean, url: string) { 227 | let size = 0 228 | 229 | function getStrSize() { 230 | if (!headersOnly) { 231 | const resType = xhr.responseType 232 | let resTxt = '' 233 | 234 | if (resType === '' || resType === 'text') resTxt = xhr.responseText 235 | if (resTxt) size = lenToUtf8Bytes(resTxt) 236 | } 237 | } 238 | 239 | if (isCrossOrig(url)) { 240 | getStrSize() 241 | } else { 242 | try { 243 | size = toNum(xhr.getResponseHeader('Content-Length')) 244 | } catch (e) { 245 | getStrSize() 246 | } 247 | } 248 | 249 | if (size === 0) getStrSize() 250 | 251 | return size 252 | } 253 | 254 | const link = document.createElement('a') 255 | 256 | export function fullUrl(href: string) { 257 | link.href = href 258 | 259 | return ( 260 | link.protocol + '//' + link.host + link.pathname + link.search + link.hash 261 | ) 262 | } 263 | 264 | function getFileName(url: string) { 265 | let ret = last(url.split('/')) 266 | 267 | if (ret.indexOf('?') > -1) ret = trim(ret.split('?')[0]) 268 | 269 | if (ret === '') { 270 | const urlObj = new Url(url) 271 | ret = urlObj.hostname 272 | } 273 | 274 | return ret 275 | } 276 | 277 | function getType(contentType: string) { 278 | if (!contentType) 279 | return { 280 | type: 'unknown', 281 | subType: 'unknown', 282 | } 283 | 284 | const type = contentType.split(';')[0].split('/') 285 | 286 | return { 287 | type: type[0], 288 | subType: last(type), 289 | } 290 | } 291 | 292 | function readBlobAsText(blob: Blob, callback: any) { 293 | const reader = new FileReader() 294 | reader.onload = () => { 295 | callback(null, reader.result) 296 | } 297 | reader.onerror = err => { 298 | callback(err) 299 | } 300 | reader.readAsText(blob) 301 | } 302 | 303 | const origin = window.location.origin 304 | 305 | function isCrossOrig(url: string) { 306 | return !startWith(url, origin) 307 | } 308 | 309 | function lenToUtf8Bytes(str: string) { 310 | const m = encodeURIComponent(str).match(/%[89ABab]/g) 311 | 312 | return str.length + (m ? m.length : 0) 313 | } 314 | -------------------------------------------------------------------------------- /src/lib/resources.ts: -------------------------------------------------------------------------------- 1 | import map from 'licia/map' 2 | import filter from 'licia/filter' 3 | import compact from 'licia/compact' 4 | import contain from 'licia/contain' 5 | import endWith from 'licia/endWith' 6 | 7 | let isPerformanceSupported = false 8 | const performance = (window as any).webkitPerformance || window.performance 9 | if (performance && performance.getEntries) { 10 | isPerformanceSupported = true 11 | } 12 | 13 | export function getScripts(): string[] { 14 | if (isPerformanceSupported) { 15 | return getResources('script') 16 | } 17 | 18 | const elements = document.querySelectorAll('script') 19 | 20 | return compact(map(elements, element => element.src)) 21 | } 22 | 23 | export function getImages(): string[] { 24 | if (isPerformanceSupported) { 25 | return getResources('img') 26 | } 27 | 28 | const elements = document.querySelectorAll('img') 29 | 30 | return compact(map(elements, element => element.src)) 31 | } 32 | 33 | export function isImage(url: string) { 34 | return contain(getImages(), url) 35 | } 36 | 37 | function getResources(type: string) { 38 | return map( 39 | filter(performance.getEntries(), (entry: any) => { 40 | if (entry.entryType !== 'resource') { 41 | return false 42 | } 43 | 44 | if (entry.initiatorType === type) { 45 | return true 46 | } else if (entry.initiatorType === 'other') { 47 | // preload 48 | if (type === 'script') { 49 | if (endWith(entry.name, '.js')) { 50 | return true 51 | } 52 | } 53 | } 54 | 55 | return false 56 | }), 57 | entry => entry.name 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/lib/scriptMananger.ts: -------------------------------------------------------------------------------- 1 | import each from 'licia/each' 2 | import strHash from 'licia/strHash' 3 | import toStr from 'licia/toStr' 4 | import { getAbsoluteUrl, getTextContent } from './util' 5 | import * as resources from './resources' 6 | 7 | const scripts = new Map() 8 | scripts.set('1', { 9 | scriptId: '1', 10 | startColumn: 0, 11 | startLine: 0, 12 | endColumn: 100000, 13 | endLine: 100000, 14 | scriptLanguage: 'JavaScript', 15 | url: '', 16 | }) 17 | const sources = new Map() 18 | sources.set('1', '') 19 | 20 | export function getScript(scriptId: string) { 21 | return scripts.get(scriptId) 22 | } 23 | 24 | export async function getScriptSource(scriptId: string, proxy = '') { 25 | if (sources.get(scriptId)) { 26 | return sources.get(scriptId) 27 | } 28 | const script = getScript(scriptId) 29 | const source = await getTextContent(script.url, proxy) 30 | sources.set(scriptId, source) 31 | 32 | return sources.get(scriptId) 33 | } 34 | 35 | export function getScripts() { 36 | const ret: any[] = [] 37 | 38 | const srcs = resources.getScripts() 39 | 40 | each(srcs, src => { 41 | const url = getAbsoluteUrl(src) 42 | const scriptId = getScriptId(url) 43 | if (!scripts.get(scriptId)) { 44 | scripts.set(scriptId, { 45 | scriptId, 46 | startColumn: 0, 47 | startLine: 0, 48 | endColumn: 100000, 49 | endLine: 100000, 50 | scriptLanguage: 'JavaScript', 51 | url, 52 | }) 53 | } 54 | ret.push(scripts.get(scriptId)) 55 | }) 56 | 57 | return ret 58 | } 59 | 60 | function getScriptId(url: string) { 61 | return toStr(strHash(url)) 62 | } 63 | -------------------------------------------------------------------------------- /src/lib/stylesheet.ts: -------------------------------------------------------------------------------- 1 | import each from 'licia/each' 2 | import Emitter from 'licia/Emitter' 3 | import strHash from 'licia/strHash' 4 | import toStr from 'licia/toStr' 5 | import trim from 'licia/trim' 6 | import cssPriority from 'licia/cssPriority' 7 | import map from 'licia/map' 8 | import { createId, getTextContent } from './util' 9 | 10 | const elProto: any = Element.prototype 11 | 12 | let matchesSel: any = () => false 13 | 14 | if (elProto.webkitMatchesSelector) { 15 | matchesSel = (el: any, selText: string) => el.webkitMatchesSelector(selText) 16 | } else if (elProto.mozMatchesSelector) { 17 | matchesSel = (el: any, selText: string) => el.mozMatchesSelector(selText) 18 | } else if (elProto.msMatchesSelector) { 19 | matchesSel = (el: any, selText: string) => el.msMatchesSelector(selText) 20 | } 21 | 22 | export function matchesSelector(el: any, selText: string) { 23 | return matchesSel(el, selText) 24 | } 25 | 26 | const emitter = new Emitter() 27 | export function onStyleSheetAdded(fn: any) { 28 | emitter.on('styleSheetAdded', fn) 29 | } 30 | 31 | export function getStyleSheets() { 32 | each(document.styleSheets, (styleSheet: any) => { 33 | if (!styleSheet.styleSheetId) { 34 | styleSheet.styleSheetId = getStyleSheetId(styleSheet.href) 35 | } 36 | }) 37 | 38 | return document.styleSheets 39 | } 40 | 41 | export function getMatchedCssRules(node: any) { 42 | const unsorted: any[] = [] 43 | 44 | each(document.styleSheets, (styleSheet: any) => { 45 | let styleSheetId = styleSheet.styleSheetId 46 | if (!styleSheetId) { 47 | styleSheetId = getStyleSheetId(styleSheet.href) 48 | styleSheet.styleSheetId = styleSheetId 49 | emitter.emit('styleSheetAdded', styleSheet) 50 | } 51 | try { 52 | // Started with version 64, Chrome does not allow cross origin script to access this property. 53 | if (!styleSheet.cssRules) return 54 | } catch (e) { 55 | return 56 | } 57 | 58 | each(styleSheet.cssRules, (cssRule: any) => { 59 | let matchesEl = false 60 | 61 | // Mobile safari will throw DOM Exception 12 error, need to try catch it. 62 | try { 63 | matchesEl = matchesSelector(node, cssRule.selectorText) 64 | } catch (e) { 65 | /* tslint:disable-next-line */ 66 | } 67 | 68 | if (!matchesEl) return 69 | 70 | unsorted.push({ 71 | selectorText: cssRule.selectorText, 72 | style: cssRule.style, 73 | styleSheetId, 74 | }) 75 | }) 76 | }) 77 | 78 | const sorted: any[] = [] 79 | const priorities = map(unsorted, ({ selectorText }, i) => { 80 | return cssPriority(selectorText, { position: i }) 81 | }) 82 | each(priorities.sort(cssPriority.compare), property => { 83 | sorted.push(unsorted[property[5]]) 84 | }) 85 | 86 | return sorted 87 | } 88 | 89 | export function formatStyle(style: any) { 90 | const ret: any = {} 91 | 92 | for (let i = 0, len = style.length; i < len; i++) { 93 | const name = style[i] 94 | 95 | ret[name] = style[name] || trim(style.getPropertyValue(name)) 96 | } 97 | 98 | return ret 99 | } 100 | 101 | const inlineStyleSheetIds = new Map() 102 | const inlineStyleNodeIds = new Map() 103 | 104 | export function getOrCreateInlineStyleSheetId(nodeId: any) { 105 | let styleSheetId = inlineStyleSheetIds.get(nodeId) 106 | if (styleSheetId) return styleSheetId 107 | 108 | styleSheetId = getStyleSheetId() 109 | inlineStyleSheetIds.set(nodeId, styleSheetId) 110 | inlineStyleNodeIds.set(styleSheetId, nodeId) 111 | 112 | return styleSheetId 113 | } 114 | 115 | export function getInlineStyleSheetId(nodeId: any) { 116 | return inlineStyleSheetIds.get(nodeId) 117 | } 118 | 119 | export function getInlineStyleNodeId(styleSheetId: string) { 120 | return inlineStyleNodeIds.get(styleSheetId) 121 | } 122 | 123 | const styleSheetTexts = new Map() 124 | 125 | export async function getStyleSheetText(styleSheetId: string, proxy = '') { 126 | if (styleSheetTexts.get(styleSheetId)) { 127 | return styleSheetTexts.get(styleSheetId) 128 | } 129 | for (let i = 0, len = document.styleSheets.length; i < len; i++) { 130 | const styleSheet: any = document.styleSheets[i] 131 | if (styleSheet.styleSheetId === styleSheetId) { 132 | const text = await getTextContent(styleSheet.href, proxy) 133 | styleSheetTexts.set(styleSheetId, text) 134 | break 135 | } 136 | } 137 | return styleSheetTexts.get(styleSheetId) || '' 138 | } 139 | 140 | function getStyleSheetId(sourceUrl = '') { 141 | if (sourceUrl) { 142 | return toStr(strHash(sourceUrl)) 143 | } 144 | 145 | return createId() 146 | } 147 | -------------------------------------------------------------------------------- /src/lib/util.ts: -------------------------------------------------------------------------------- 1 | import uniqId from 'licia/uniqId' 2 | import random from 'licia/random' 3 | import startWith from 'licia/startWith' 4 | import Url from 'licia/Url' 5 | import convertBin from 'licia/convertBin' 6 | import axios from 'axios' 7 | import _type from 'licia/type' 8 | import _has from 'licia/has' 9 | 10 | const prefix = random(1000, 9999) + '.' 11 | 12 | export function createId() { 13 | return uniqId(prefix) 14 | } 15 | 16 | export function getAbsoluteUrl(url: string) { 17 | const a = document.createElement('a') 18 | a.href = url 19 | return a.href 20 | } 21 | 22 | export class ErrorWithCode extends Error { 23 | code: number 24 | constructor(code: number, message: string) { 25 | super(message) 26 | this.code = code 27 | 28 | Object.setPrototypeOf(this, new.target.prototype) 29 | } 30 | } 31 | 32 | export function createErr(code: number, message: string) { 33 | return new ErrorWithCode(code, message) 34 | } 35 | 36 | export function getUrl() { 37 | const href = location.href 38 | if (startWith(href, 'about:')) { 39 | return parent.location.href 40 | } 41 | return href 42 | } 43 | 44 | export function getOrigin() { 45 | const origin = location.origin 46 | if (origin === 'null') { 47 | return parent.location.origin 48 | } 49 | return origin 50 | } 51 | 52 | export async function getTextContent(url: string, proxy = '') { 53 | return await getContent(url, 'text', proxy) 54 | } 55 | 56 | export async function getBase64Content(url: string, proxy = '') { 57 | return convertBin(await getContent(url, 'arraybuffer', proxy), 'base64') 58 | } 59 | 60 | export function getType(val: any, lowerCase: boolean) { 61 | try { 62 | return _type.apply(null, [val, lowerCase]) 63 | } catch (e) { 64 | return 'Error' 65 | } 66 | } 67 | 68 | export function has(obj: any, key: string) { 69 | try { 70 | return _has.apply(null, [obj, key]) 71 | } catch (e) { 72 | return false 73 | } 74 | } 75 | 76 | async function getContent(url: string, responseType: any, proxy = '') { 77 | try { 78 | const urlObj = new Url(url) 79 | urlObj.setQuery('__chobitsu-hide__', 'true') 80 | const result = await axios.get(urlObj.toString(), { 81 | responseType, 82 | }) 83 | return result.data 84 | } catch (e) { 85 | if (proxy) { 86 | try { 87 | const result = await axios.get(proxyUrl(proxy, url), { 88 | responseType, 89 | }) 90 | return await result.data 91 | } catch (e) { 92 | /* eslint-disable */ 93 | } 94 | } 95 | } 96 | 97 | return responseType === 'arraybuffer' ? new ArrayBuffer(0) : '' 98 | } 99 | 100 | function proxyUrl(proxy: string, url: string) { 101 | const urlObj = new Url(proxy) 102 | urlObj.setQuery('url', url) 103 | urlObj.setQuery('__chobitsu-hide__', 'true') 104 | return urlObj.toString() 105 | } 106 | -------------------------------------------------------------------------------- /test/CSS.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/chobitsu/ac90c6ce3b9f51a9268e95f1427f9337d0eaa848/test/CSS.spec.js -------------------------------------------------------------------------------- /test/DOM.spec.js: -------------------------------------------------------------------------------- 1 | describe('DOM', () => { 2 | it('getNodeId', async () => { 3 | const node = document.createElement('div') 4 | const domain = chobitsu.domain('DOM') 5 | const { nodeId } = domain.getNodeId({ node }) 6 | expect(nodeId).to.be.a('number') 7 | expect(domain.getOuterHTML({ nodeId }).outerHTML).to.equal(node.outerHTML) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /test/DOMDebugger.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/chobitsu/ac90c6ce3b9f51a9268e95f1427f9337d0eaa848/test/DOMDebugger.spec.js -------------------------------------------------------------------------------- /test/DOMStorage.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/chobitsu/ac90c6ce3b9f51a9268e95f1427f9337d0eaa848/test/DOMStorage.spec.js -------------------------------------------------------------------------------- /test/Debugger.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/chobitsu/ac90c6ce3b9f51a9268e95f1427f9337d0eaa848/test/Debugger.spec.js -------------------------------------------------------------------------------- /test/Network.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/chobitsu/ac90c6ce3b9f51a9268e95f1427f9337d0eaa848/test/Network.spec.js -------------------------------------------------------------------------------- /test/Overlay.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/chobitsu/ac90c6ce3b9f51a9268e95f1427f9337d0eaa848/test/Overlay.spec.js -------------------------------------------------------------------------------- /test/Page.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/chobitsu/ac90c6ce3b9f51a9268e95f1427f9337d0eaa848/test/Page.spec.js -------------------------------------------------------------------------------- /test/Runtime.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/chobitsu/ac90c6ce3b9f51a9268e95f1427f9337d0eaa848/test/Runtime.spec.js -------------------------------------------------------------------------------- /test/Storage.spec.js: -------------------------------------------------------------------------------- 1 | describe('Storage', () => { 2 | it('clearDataForOrigin', async () => { 3 | localStorage.setItem('name', 'chobitsu') 4 | sessionStorage.setItem('license', 'mit') 5 | await chobitsu.sendMessage('Storage.clearDataForOrigin', { 6 | storageTypes: 'local_storage', 7 | }) 8 | expect(localStorage.getItem('name')).to.be.null 9 | expect(sessionStorage.getItem('license')).to.be.null 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "outDir": "./dist/cjs", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "strictPropertyInitialization": false, 8 | "removeComments": true, 9 | "noUnusedLocals": true, 10 | "target": "ES5", 11 | "declaration": true, 12 | "esModuleInterop": true, 13 | "lib": ["DOM", "ES2015"] 14 | }, 15 | "include": ["src/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const pkg = require('./package.json') 4 | 5 | const banner = pkg.name + ' v' + pkg.version + ' ' + pkg.homepage 6 | 7 | module.exports = { 8 | entry: './src/index.ts', 9 | devtool: 'source-map', 10 | output: { 11 | filename: 'chobitsu.js', 12 | path: path.resolve(__dirname, 'dist'), 13 | library: 'chobitsu', 14 | libraryExport: 'default', 15 | libraryTarget: 'umd', 16 | }, 17 | devServer: { 18 | static: { 19 | directory: path.join(__dirname, 'devtools'), 20 | watch: false, 21 | }, 22 | port: 8080, 23 | }, 24 | resolve: { 25 | extensions: ['.ts', '.js'], 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | use: ['ts-loader'], 32 | }, 33 | { 34 | test: /\.js$/, 35 | use: { 36 | loader: 'babel-loader', 37 | options: { 38 | presets: ['@babel/preset-env'], 39 | }, 40 | }, 41 | }, 42 | { 43 | test: /\.css$/, 44 | use: [ 45 | { 46 | loader: 'raw-loader', 47 | options: { 48 | esModule: false, 49 | }, 50 | }, 51 | ], 52 | }, 53 | ], 54 | }, 55 | plugins: [new webpack.BannerPlugin(banner)], 56 | } 57 | --------------------------------------------------------------------------------