├── .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 | Reload
15 | Click
16 | Touch
17 |
30 |
31 |
32 | I'm in the shadow DOM
33 |
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 |
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(`${localName}>`, 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 |
--------------------------------------------------------------------------------