├── .babelrc.js ├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .husky └── pre-push ├── .npmrc ├── LICENSE ├── README.md ├── package.json ├── pnpm-lock.yaml ├── rollup.config.mjs ├── src ├── global.d.ts ├── index.ts └── solid.ts └── tsconfig.json /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: require.resolve('@gera2ld/plaid/config/babelrc-base'), 3 | presets: ['@babel/preset-typescript'], 4 | }; 5 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | Chrome >= 55 2 | Firefox >= 53 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | quote_type = single 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/src 3 | !/test 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | require.resolve('@gera2ld/plaid/eslint'), 5 | ], 6 | parserOptions: { 7 | project: './tsconfig.json', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npmjs 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - uses: pnpm/action-setup@v4 15 | name: Install pnpm 16 | with: 17 | version: 9 18 | run_install: false 19 | 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 22 23 | cache: 'pnpm' 24 | registry-url: 'https://registry.npmjs.org' 25 | 26 | - run: pnpm i && pnpm publish --no-git-checks 27 | env: 28 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | /.idea 4 | /dist 5 | /.nyc_output 6 | /coverage 7 | /types 8 | /docs 9 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npm run lint 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist = true 2 | strict-peer-dependencies = false 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gerald 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 | # @violentmonkey/dom 2 | 3 | [![NPM](https://img.shields.io/npm/v/@violentmonkey/dom.svg)](https://npm.im/@violentmonkey/dom) 4 | ![License](https://img.shields.io/npm/l/@violentmonkey/dom.svg) 5 | [![jsDocs.io](https://img.shields.io/badge/jsDocs.io-reference-blue)](https://www.jsdocs.io/package/@violentmonkey/dom) 6 | 7 | Use JSX for HTML elements. 8 | 9 | Based on [@gera2ld/jsx-dom](https://github.com/gera2ld/jsx-dom). 10 | 11 | ## What is it? 12 | 13 | This library is just a light wrapper around `document.createElement`. So we can easily create DOM elements using JSX with the help of this library instead of writing tedious imperative code. 14 | 15 | ## When should we NOT use it? 16 | 17 | You should NOT use it when you use a library that has its own implementation of JSX, such as React, Vue, Svelte, SolidJS, etc. The JSX syntaxes are incompatible and using them together will cause unexpected issues. 18 | 19 | You don't need it if you initialize a userscript project with [generator-userscript](https://github.com/violentmonkey/generator-userscript), which uses [solid-js](https://solidjs.com/). 20 | 21 | However, you can still use methods like `VM.h` directly **without using JSX** to make the code shorter. 22 | 23 | ## Usage 24 | 25 | First, include `@violentmonkey/dom` as a dependency: 26 | 27 | ```js 28 | // ... 29 | // @require https://cdn.jsdelivr.net/npm/@violentmonkey/dom@2 30 | // ... 31 | ``` 32 | 33 | Then you can use `VM.h` (similar to `React.createElement`) and `VM.m` (for `mount`ing as DOM elements) directly. 34 | 35 | There is also a `VM.hm` for `VM.h` plus `VM.m` if you don't need SVG support and want to get rid of `VM.m`. 36 | 37 | ```js 38 | const vdom = VM.h('div', {}, 'hello'); 39 | const el = VM.m(vdom); // -> HTMLDivElement 40 | 41 | // or 42 | const el = VM.hm('div', {}, 'hello'); // -> HTMLDivElement 43 | 44 | document.body.appendChild(el); 45 | ``` 46 | 47 | Or use with JSX and bundlers, for example: 48 | 49 | ```js 50 | // .babelrc.js 51 | { 52 | plugins: [ 53 | // JSX 54 | ['@babel/plugin-transform-react-jsx', { 55 | pragma: 'VM.h', // or 'VM.hm' if you don't need SVG support and want to get rid of 'VM.m' 56 | pragmaFrag: 'VM.Fragment', 57 | }], 58 | ], 59 | } 60 | ``` 61 | 62 | ```js 63 | // pragma: VM.h 64 | document.body.appendChild(VM.m(
hello
)); 65 | 66 | // pragma: VM.hm 67 | document.body.appendChild(
hello
); 68 | ``` 69 | 70 | ## API 71 | 72 | See [![jsDocs.io](https://img.shields.io/badge/jsDocs.io-reference-blue)](https://www.jsdocs.io/package/@violentmonkey/dom). 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@violentmonkey/dom", 3 | "version": "2.2.1", 4 | "description": "Use JSX for HTML elements.", 5 | "author": "Gerald ", 6 | "license": "ISC", 7 | "repository": "git@github.com:violentmonkey/vm-dom.git", 8 | "scripts": { 9 | "prepare": "husky || true", 10 | "dev": "rollup -wc", 11 | "build:types": "tsc", 12 | "build:js": "rollup -c", 13 | "build": "run-s ci clean build:*", 14 | "format": "prettier --ignore-path .eslintignore --write .", 15 | "lint": "prettier --ignore-path .eslintignore --check . && eslint --ext .ts,tsx src", 16 | "prepublishOnly": "run-s build", 17 | "ci": "run-s lint", 18 | "clean": "del-cli dist types" 19 | }, 20 | "publishConfig": { 21 | "access": "public", 22 | "registry": "https://registry.npmjs.org/" 23 | }, 24 | "unpkg": "dist/index.js", 25 | "jsdelivr": "dist/index.js", 26 | "main": "dist/index.js", 27 | "module": "dist/index.mjs", 28 | "exports": { 29 | ".": { 30 | "import": "./dist/index.mjs", 31 | "default": "./dist/index.js", 32 | "types": "./types/index.d.ts" 33 | }, 34 | "./solid-js": "./dist/solid.js" 35 | }, 36 | "files": [ 37 | "dist", 38 | "types" 39 | ], 40 | "typings": "types/index.d.ts", 41 | "devDependencies": { 42 | "@gera2ld/plaid": "~2.7.0", 43 | "@gera2ld/plaid-rollup": "~2.7.0", 44 | "del-cli": "^6.0.0", 45 | "husky": "^9.1.6" 46 | }, 47 | "dependencies": { 48 | "@babel/runtime": "^7.25.9", 49 | "@gera2ld/jsx-dom": "^2.2.2", 50 | "solid-js": "^1.9.5" 51 | }, 52 | "engines": { 53 | "node": ">=20" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineExternal, definePlugins } from '@gera2ld/plaid-rollup'; 2 | import { defineConfig } from 'rollup'; 3 | import solidPkg from 'solid-js/package.json' with { type: 'json' }; 4 | import pkg from './package.json' with { type: 'json' }; 5 | 6 | const external = defineExternal(Object.keys(pkg.dependencies)); 7 | const bundleOptions = { 8 | extend: true, 9 | esModule: false, 10 | }; 11 | const outputOptions = { 12 | indent: false, 13 | banner: `/*! ${pkg.name}@${pkg.version} | ${pkg.license} License */`, 14 | }; 15 | const replaceValues = { 16 | 'process.env.VERSION': pkg.version, 17 | }; 18 | 19 | export default defineConfig([ 20 | { 21 | input: 'src/index.ts', 22 | plugins: definePlugins({ 23 | replaceValues, 24 | }), 25 | external, 26 | output: { 27 | format: 'esm', 28 | file: `dist/index.mjs`, 29 | ...outputOptions, 30 | }, 31 | }, 32 | { 33 | input: 'src/index.ts', 34 | plugins: definePlugins({ 35 | replaceValues, 36 | }), 37 | output: { 38 | format: 'iife', 39 | file: `dist/index.js`, 40 | name: 'VM', 41 | ...outputOptions, 42 | ...bundleOptions, 43 | }, 44 | }, 45 | { 46 | input: 'src/solid.ts', 47 | plugins: definePlugins({ 48 | replaceValues, 49 | }), 50 | output: { 51 | format: 'iife', 52 | file: `dist/solid.js`, 53 | name: 'VM.solid', 54 | ...outputOptions, 55 | ...bundleOptions, 56 | banner: `/*! ${pkg.name}@${pkg.version}/solid-js solid-js@${solidPkg.version} | ${solidPkg.license} License */`, 57 | }, 58 | }, 59 | ]); 60 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare const VM: { 2 | versions: Record; 3 | }; 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | h, 3 | createElement, 4 | Fragment, 5 | mountDom, 6 | mountDom as m, 7 | hm, 8 | } from '@gera2ld/jsx-dom'; 9 | 10 | export const versions = Object.assign( 11 | (typeof VM !== 'undefined' && VM?.versions) || {}, 12 | { 13 | dom: 'process.env.VERSION', 14 | }, 15 | ); 16 | 17 | /** 18 | * Return all elements that match the given `xpath` as an array. 19 | */ 20 | export function getElementsByXPath(xpath: string, context: Node = document) { 21 | const iterator = document.evaluate( 22 | xpath, 23 | context, 24 | null, 25 | XPathResult.ANY_TYPE, 26 | null, 27 | ); 28 | const result: Node[] = []; 29 | let item: Node; 30 | while ((item = iterator.iterateNext())) { 31 | result.push(item); 32 | } 33 | return result; 34 | } 35 | 36 | /** 37 | * Walk a node tree and return all text contents in an array. 38 | */ 39 | export function getTextValues(node: HTMLElement) { 40 | if (node.nodeType === Node.TEXT_NODE) return [node.nodeValue]; 41 | if ( 42 | node.nodeType === Node.ELEMENT_NODE && 43 | !['script', 'style'].includes(node.tagName.toLowerCase()) 44 | ) { 45 | return Array.from(node.childNodes).flatMap(getTextValues); 46 | } 47 | return []; 48 | } 49 | 50 | /** 51 | * Observe an existing `node` until `callback` returns `true`. 52 | * The returned function can be called explicitly to disconnect the observer. 53 | * 54 | * ```js 55 | * VM.observe(document.body, () => { 56 | * const node = document.querySelector('.profile'); 57 | * if (node) { 58 | * console.log('It\'s there!'); 59 | * return true; 60 | * } 61 | * }); 62 | * ``` 63 | */ 64 | export function observe( 65 | node: Node, 66 | callback: ( 67 | mutations: MutationRecord[], 68 | observer: MutationObserver, 69 | ) => boolean | void, 70 | options?: MutationObserverInit, 71 | ): () => void { 72 | const observer = new MutationObserver((mutations, ob) => { 73 | const result = callback(mutations, ob); 74 | if (result) disconnect(); 75 | }); 76 | observer.observe( 77 | node, 78 | Object.assign( 79 | { 80 | childList: true, 81 | subtree: true, 82 | }, 83 | options, 84 | ), 85 | ); 86 | const disconnect = () => observer.disconnect(); 87 | return disconnect; 88 | } 89 | -------------------------------------------------------------------------------- /src/solid.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is just a light wrapper for a UMD bundle of solid-js. 3 | * The size of userscripts can be significantly reduced by using a CDN hosted UMD bundle of solid-js. 4 | */ 5 | export * from 'solid-js'; 6 | export * as store from 'solid-js/store'; 7 | export * as web from 'solid-js/web'; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "es6", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "outDir": "types", 9 | "allowSyntheticDefaultImports": true, 10 | "jsx": "react", 11 | "lib": ["DOM", "ES6", "DOM.Iterable", "ScriptHost", "ESNext"] 12 | }, 13 | "include": ["src/**/*.ts"] 14 | } 15 | --------------------------------------------------------------------------------