├── cjs ├── package.json └── index.js ├── .npmrc ├── .gitignore ├── .npmignore ├── rollup ├── index.config.js └── es.config.js ├── tsconfig.json ├── es.js ├── LICENSE ├── .github └── workflows │ └── node.js.yml ├── types └── index.d.ts ├── esm └── index.js ├── index.js ├── package.json ├── README.md └── test ├── cover.cjs └── index.html /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | package-lock=true 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | coverage/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .github/ 3 | coverage/ 4 | node_modules/ 5 | rollup/ 6 | test/ 7 | tsconfig.json 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /rollup/index.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | 3 | export default { 4 | input: './esm/index.js', 5 | plugins: [ 6 | nodeResolve() 7 | ], 8 | output: { 9 | file: './index.js', 10 | format: 'module' 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES2020", 4 | "target": "ES2020", 5 | "allowJs": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "declarationDir": "types" 9 | }, 10 | "include": [ 11 | "esm/index.js" 12 | ] 13 | } -------------------------------------------------------------------------------- /es.js: -------------------------------------------------------------------------------- 1 | const e=(e,t=document)=>t.querySelector(e),t=(e,t=document)=>[...t.querySelectorAll(e)],o=(e,t=document)=>{const o=(new XPathEvaluator).createExpression(e).evaluate(t,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE),n=[];for(let e=0,{snapshotLength:t}=o;e root.querySelector(css); 8 | 9 | /** 10 | * Given a CSS selector, returns a list of all matching nodes. 11 | * @param {string} css the CSS selector to query 12 | * @param {Document | DocumentFragment | Element} [root] the optional parent node to query 13 | * @returns {Element[]} a list of found nodes 14 | */ 15 | const $$ = (css, root = document) => [...root.querySelectorAll(css)]; 16 | 17 | /** 18 | * Given a XPath selector, returns a list of all matching nodes. 19 | * @param {string} path the XPath selector to evaluate 20 | * @param {Document | DocumentFragment | Element} [root] the optional parent node to query 21 | * @returns {Node[]} a list of found nodes (elements, attributes, text, comments) 22 | */ 23 | const $x = (path, root = document) => { 24 | const expression = (new XPathEvaluator).createExpression(path); 25 | const xpath = expression.evaluate(root, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); 26 | const result = []; 27 | for (let i = 0, {snapshotLength} = xpath; i < snapshotLength; i++) 28 | result.push(xpath.snapshotItem(i)); 29 | return result; 30 | }; 31 | 32 | export {$, $$, $x}; 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Given a CSS selector, returns the first matching node, if any. 3 | * @param {string} css the CSS selector to query 4 | * @param {Document | DocumentFragment | Element} [root] the optional parent node to query 5 | * @returns {Element?} the found element, if any 6 | */ 7 | const $ = (css, root = document) => root.querySelector(css); 8 | 9 | /** 10 | * Given a CSS selector, returns a list of all matching nodes. 11 | * @param {string} css the CSS selector to query 12 | * @param {Document | DocumentFragment | Element} [root] the optional parent node to query 13 | * @returns {Element[]} a list of found nodes 14 | */ 15 | const $$ = (css, root = document) => [...root.querySelectorAll(css)]; 16 | 17 | /** 18 | * Given a XPath selector, returns a list of all matching nodes. 19 | * @param {string} path the XPath selector to evaluate 20 | * @param {Document | DocumentFragment | Element} [root] the optional parent node to query 21 | * @returns {Node[]} a list of found nodes (elements, attributes, text, comments) 22 | */ 23 | const $x = (path, root = document) => { 24 | const expression = (new XPathEvaluator).createExpression(path); 25 | const xpath = expression.evaluate(root, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); 26 | const result = []; 27 | for (let i = 0, {snapshotLength} = xpath; i < snapshotLength; i++) 28 | result.push(xpath.snapshotItem(i)); 29 | return result; 30 | }; 31 | 32 | export { $, $$, $x }; 33 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Given a CSS selector, returns the first matching node, if any. 4 | * @param {string} css the CSS selector to query 5 | * @param {Document | DocumentFragment | Element} [root] the optional parent node to query 6 | * @returns {Element?} the found element, if any 7 | */ 8 | const $ = (css, root = document) => root.querySelector(css); 9 | 10 | /** 11 | * Given a CSS selector, returns a list of all matching nodes. 12 | * @param {string} css the CSS selector to query 13 | * @param {Document | DocumentFragment | Element} [root] the optional parent node to query 14 | * @returns {Element[]} a list of found nodes 15 | */ 16 | const $$ = (css, root = document) => [...root.querySelectorAll(css)]; 17 | 18 | /** 19 | * Given a XPath selector, returns a list of all matching nodes. 20 | * @param {string} path the XPath selector to evaluate 21 | * @param {Document | DocumentFragment | Element} [root] the optional parent node to query 22 | * @returns {Node[]} a list of found nodes (elements, attributes, text, comments) 23 | */ 24 | const $x = (path, root = document) => { 25 | const expression = (new XPathEvaluator).createExpression(path); 26 | const xpath = expression.evaluate(root, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); 27 | const result = []; 28 | for (let i = 0, {snapshotLength} = xpath; i < snapshotLength; i++) 29 | result.push(xpath.snapshotItem(i)); 30 | return result; 31 | }; 32 | 33 | exports.$ = $; 34 | exports.$$ = $$; 35 | exports.$x = $x; 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-devtools", 3 | "version": "0.1.6", 4 | "description": "Exports `$`, `$$`, and `$x` utilities as described in Chrome Console Utilities API reference", 5 | "main": "./cjs/index.js", 6 | "scripts": { 7 | "build": "npm run cjs && npm run rollup:es && npm run rollup:index && npm run tsc && npm run test && npm run size", 8 | "cjs": "ascjs --no-default esm cjs", 9 | "coverage": "mkdir -p ./coverage/tmp; c8 report --reporter=text-lcov > ./coverage/lcov.info", 10 | "rollup:es": "rollup --config rollup/es.config.js", 11 | "rollup:index": "rollup --config rollup/index.config.js", 12 | "size": "cat es.js | brotli | wc -c", 13 | "test": "c8 node test/cover.cjs", 14 | "tsc": "tsc -p ." 15 | }, 16 | "keywords": [ 17 | "$", 18 | "$$", 19 | "$x", 20 | "devtools", 21 | "utilities" 22 | ], 23 | "author": "Andrea Giammarchi", 24 | "license": "ISC", 25 | "devDependencies": { 26 | "@rollup/plugin-node-resolve": "^15.0.2", 27 | "@rollup/plugin-terser": "^0.4.1", 28 | "ascjs": "^5.0.1", 29 | "c8": "^7.13.0", 30 | "rollup": "^3.21.0", 31 | "typescript": "^5.0.4" 32 | }, 33 | "module": "./esm/index.js", 34 | "type": "module", 35 | "types": "./types/index.d.ts", 36 | "exports": { 37 | ".": { 38 | "types": "./types/index.d.ts", 39 | "import": "./esm/index.js", 40 | "default": "./cjs/index.js" 41 | }, 42 | "./package.json": "./package.json" 43 | }, 44 | "unpkg": "es.js", 45 | "repository": { 46 | "type": "git", 47 | "url": "git+https://github.com/WebReflection/basic-devtools.git" 48 | }, 49 | "bugs": { 50 | "url": "https://github.com/WebReflection/basic-devtools/issues" 51 | }, 52 | "homepage": "https://github.com/WebReflection/basic-devtools#readme" 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # basic-devtools 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/WebReflection/basic-devtools/badge.svg?branch=main)](https://coveralls.io/github/WebReflection/basic-devtools?branch=main) [![build status](https://github.com/WebReflection/basic-devtools/actions/workflows/node.js.yml/badge.svg)](https://github.com/WebReflection/basic-devtools/actions) 4 | 5 | **Social Media Photo by [Basic Moto France](https://unsplash.com/@basic_moto) on [Unsplash](https://unsplash.com/)** 6 | 7 | Exports `$`, `$$`, and `$x` utilities as described in [Chrome Console Utilities API reference](https://developer.chrome.com/docs/devtools/console/utilities/), all in 242 bytes once "*minzipped*" or 206 bytes once "*minbrotlied*". 8 | 9 | ```js 10 | import {$, $$, $x} from 'basic-devtools'; 11 | 12 | // single node 13 | $('nope') === null; // true 14 | $('body') === document.body; // true 15 | 16 | // list of nodes 17 | $$('body').length === 1; // true 18 | $$('body')[0] === document.body; // true 19 | 20 | // list of evaluated nodes 21 | $x('//body').length === 1; // true 22 | $x('//body')[0] === document.body; // true 23 | ``` 24 | 25 | ### What's the deal with XPath? 26 | 27 | It's extremely powerful but also generally faster than a *TreeWalker*, as you can [**test live**](https://webreflection.github.io/basic-devtools/test/). 28 | 29 | As example, let's consider this **Question**: 30 | 31 | > "_How would I grab all data-* attributes and reach their element with a single-pass query?_" 32 | 33 | **Answer** 34 | 35 | ```js 36 | // grab all nodes with data-* attributes 37 | const allDataAttributes = $x('//@*[starts-with(name(), "data-")]'); 38 | 39 | // loop all returned attributes and do something 40 | for (const {name, value, ownerElement} of allDataAttributes) { 41 | // ownerElement is the element using data-* attribute 42 | // name is the data-* attribute name 43 | // value is its value 44 | } 45 | ``` 46 | 47 | You can have a gist of various other features via this awesome [Xpath cheatsheet](https://devhints.io/xpath). 48 | -------------------------------------------------------------------------------- /test/cover.cjs: -------------------------------------------------------------------------------- 1 | // dummy XPath utility used only to code cover results 2 | // realt tests are done via browser 3 | Object.assign(globalThis, { 4 | XPathResult: {ORDERED_NODE_SNAPSHOT_TYPE: 7}, 5 | XPathEvaluator: class { 6 | createExpression(path) { 7 | return { 8 | evaluate(root) { 9 | const list = $$(path.replace(/[^a-z*]+/g, ''), root); 10 | return { 11 | snapshotLength: list.length, 12 | snapshotItem: i => list[i] 13 | }; 14 | } 15 | }; 16 | } 17 | }, 18 | document: { 19 | body: { 20 | firstElementChild: {}, 21 | get querySelector() { 22 | return querySelector.bind(document); 23 | }, 24 | get querySelectorAll() { 25 | return querySelectorAll.bind(document); 26 | } 27 | }, 28 | nope: null, 29 | get ['*']() { 30 | return this.body.firstElementChild; 31 | }, 32 | querySelector, 33 | querySelectorAll 34 | } 35 | }); 36 | 37 | // 38 | const {$, $$, $x} = require('../cjs'); 39 | console.assert($('nope') === null, '$("nope")'); 40 | console.assert($('body') === document.body, '$("body")'); 41 | console.assert($$('body').length === 1, '$$("body").length'); 42 | console.assert($$('body')[0] === document.body, '$$("body")[0]'); 43 | console.assert($x('//body').length === 1, '$x("//body").length'); 44 | console.assert($x('//body')[0] === document.body, '$x("//body")[0]'); 45 | console.assert($('*', document.body) === document.body.firstElementChild, '$("*", document.body)'); 46 | console.assert($$('*', document.body).length === 1, '$$("*", document.body).length'); 47 | console.assert($$('*', document.body)[0] === document.body.firstElementChild, '$$("*", document.body)[0]'); 48 | console.assert($x('.//*', document.body).length === 1, '$x(".//*", document.body).length'); 49 | console.assert($x('.//*', document.body)[0] === document.body.firstElementChild, '$x(".//*", document.body)[0]'); 50 | // 51 | 52 | function querySelector(css) { 53 | return this[css]; 54 | } 55 | 56 | function querySelectorAll(css) { 57 | return css === '*' ? [this.body.firstElementChild] : [this.body]; 58 | } 59 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TreeWalker vs $x Benchmark 8 | 73 | 74 |
testing
75 | --------------------------------------------------------------------------------