├── .github ├── FUNDING.yml ├── issue_template.md └── workflows │ ├── CI.yml │ └── Publish.yml ├── .gitignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── parser.d.ts ├── parser.test.ts ├── pnpm-lock.yaml ├── shim.d.ts ├── shim.test.ts ├── strict.d.ts ├── strict.test.ts └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: g-plane 2 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Test 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v4 12 | - name: Setup pnpm 13 | uses: pnpm/action-setup@v4.0.0 14 | with: 15 | version: latest 16 | run_install: true 17 | - name: Code style check 18 | run: pnpm fmt:check 19 | - name: Run tests 20 | run: pnpm test 21 | user_test: 22 | name: User Test 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | with: 28 | path: ./g-plane/typed-query-selector 29 | - name: Checkout "refined-github" 30 | uses: actions/checkout@v4 31 | with: 32 | repository: refined-github/refined-github 33 | ref: f462a67be700d790d5e4ea988479e0883d585e7d 34 | path: ./refined-github/refined-github 35 | - name: Install dependencies 36 | run: npm ci 37 | working-directory: ./refined-github/refined-github 38 | - name: Link package 39 | run: | 40 | cd ./g-plane/typed-query-selector 41 | sudo npm link 42 | cd ../../refined-github/refined-github 43 | npm link typed-query-selector 44 | - name: Execute in non-strict mode 45 | run: node node_modules/.bin/tsc --noEmit --extendedDiagnostics 46 | working-directory: ./refined-github/refined-github 47 | - name: Execute in strict mode 48 | run: | 49 | sed -i "s/typed-query-selector/typed-query-selector\/strict/" source/refined-github.ts 50 | node node_modules/.bin/tsc --noEmit --extendedDiagnostics 51 | working-directory: ./refined-github/refined-github 52 | -------------------------------------------------------------------------------- /.github/workflows/Publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | id-token: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | registry-url: 'https://registry.npmjs.org' 19 | - run: npm publish --provenance --access public 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Unreleased 4 | 5 | ## v2.12.0 6 | 7 | - Added support of special case for custom pseudo element selector. 8 | 9 | ## v2.11.4 10 | 11 | - Fixed parsing unclosed attribute selector (such as `a[href`). 12 | - Fixed parsing bare pseudo class or element selectors. 13 | 14 | ## v2.11.3 15 | 16 | - Fixed for TypeScript 5.5. 17 | 18 | ## v2.11.2 19 | 20 | - Fixed handling `unknown` incorrectly. (Fix [#36](https://github.com/g-plane/typed-query-selector/issues/36)) 21 | 22 | ## v2.11.1 23 | 24 | - Fixed for TypeScript 5.4. (Fix [#35](https://github.com/g-plane/typed-query-selector/issues/35)) 25 | 26 | ## v2.11.0 27 | 28 | - Added `.js` extension in import statements. 29 | 30 | ## v2.10.1 31 | 32 | - Setup publishing on GitHub Actions. No code changes. 33 | 34 | ## v2.10.0 35 | 36 | - Reverted to fallback to `Element`, not `HTMLElement`. 37 | 38 | ## v2.9.2 39 | 40 | - Fixed regression of specifying fallback type. (Fix [#33](https://github.com/g-plane/typed-query-selector/issues/33)) 41 | 42 | ## v2.9.1 43 | 44 | - Fixed some cases of `:is()` and `:where()` by rewriting the expanding logic. 45 | - Fixed template with dynamic interpolation. 46 | 47 | ## v2.9.0 48 | 49 | - Added fallback support to strict parser. 50 | - Fallback to `HTMLElement` for `HTMLElement`, `Document` and `DocumentFragment`. (Close [#29](https://github.com/g-plane/typed-query-selector/issues/29)) 51 | 52 | ## v2.8.1 53 | 54 | - Fixed passing `string` type as argument in strict parser. 55 | - Fixed some cases of `:is()` and `:where()`. 56 | 57 | ## v2.8.0 58 | 59 | - Export "strict" parser. (Close [#25](https://github.com/g-plane/typed-query-selector/issues/25)) 60 | - Removed exported private type `ParseSelectorToTagNames`. 61 | 62 | ## v2.7.0 63 | 64 | - Require TypeScript 4.7. 65 | - Fixed invalid result after expanding `:is` or `:where` function. (Fix [#23](https://github.com/g-plane/typed-query-selector/issues/23)) 66 | 67 | ## v2.6.1 68 | 69 | - Fixed support of TypeScript 4.5. 70 | 71 | ## v2.6.0 72 | 73 | - Support `.closest()` method on `Element`. (Close [#16](https://github.com/g-plane/typed-query-selector/issues/16)) 74 | 75 | ## v2.5.3 76 | 77 | - Fixed a regression bug which is introduced in v2.5.2. 78 | 79 | ## v2.5.2 80 | 81 | - Fixed attribute syntax in `:is()` and `:where()`. (Fix [#15](https://github.com/g-plane/typed-query-selector/issues/15)) 82 | 83 | ## v2.5.1 84 | 85 | - Fixed regression bug: fallback type doesn't work. (Fix [#14](https://github.com/g-plane/typed-query-selector/issues/14)) 86 | 87 | ## v2.5.0 88 | 89 | - Added support for `:is()` and `:where()` pseudo-classes. (Fix [#13](https://github.com/g-plane/typed-query-selector/issues/13)) 90 | 91 | ## v2.4.1 92 | 93 | - Fixed return type in shim can be cast. 94 | 95 | ## v2.4.0 96 | 97 | - Fixed errors with disabled `skipLibCheck`. 98 | - Allowed to specify custom fallback type when failed to parse selector. 99 | (only for `ParseSelector` type, not for `querySelector` function) 100 | 101 | ## v2.3.0 102 | 103 | - Treat empty string as syntax error. 104 | - Treat empty attribute `[]` as syntax error. 105 | - Added namespace prefix support. 106 | - Added strict mode. 107 | - Fixed precedences between combinators and groupings. 108 | 109 | ## v2.2.4 110 | 111 | - Process quotes separately. 112 | - Added simple selector syntax check. 113 | 114 | ## v2.2.3 115 | 116 | - Refactor types. 117 | 118 | ## v2.2.2 119 | 120 | - Fixed possible OOM. 121 | - Fixed arbitrary content in attribute. 122 | 123 | ## v2.2.1 124 | 125 | - Fixed multiline with starting tabs or spaces. 126 | 127 | ## v2.2.0 128 | 129 | - Support tabs in selector string. 130 | - Support pseudo-classes in selector. 131 | 132 | ## v2.1.1 133 | 134 | - Fixed more kinds of whitespace characters in selector. 135 | - Fixed combinator characters in attribute of selector. 136 | 137 | ## v2.1.0 138 | 139 | - Added `types` field to package.json. 140 | 141 | ## v2.0.0 142 | 143 | - **Breaking change**: `querySelector` and `querySelectorAll` from this package are removed. 144 | You just need to import this package then use `document.querySelector` normally. 145 | Check out readme for detailed usage. 146 | 147 | ## v1.1.0 148 | 149 | - Publish declaration map. 150 | 151 | ## v1.0.0 152 | 153 | - Initial release. 154 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present Pig Fang 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 | # 🏷 Typed `querySelector` 2 | 3 | `querySelector` and `querySelectorAll` functions with better typing 4 | by leveraging TypeScript 4.1 template literal type. 5 | 6 | ## 💿 Install 7 | 8 | ``` 9 | npm i -D typed-query-selector 10 | ``` 11 | 12 | ## 🍉 Usage 13 | 14 | ### Automatic shim 15 | 16 | All you need to do is to import this module, 17 | then the `querySelector` and `querySelectorAll` function will be enhanced. 18 | 19 | This module only works at type level and doesn't have any runtime code. 20 | 21 | ```typescript 22 | import 'typed-query-selector' 23 | 24 | document.querySelector('div#app') // ==> HTMLDivElement 25 | 26 | document.querySelector('div#app > form#login') // ==> HTMLFormElement 27 | 28 | document.querySelectorAll('span.badge') // ==> NodeListOf 29 | 30 | anElement.querySelector('button#submit') // ==> HTMLButtonElement 31 | ``` 32 | 33 | _[Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAchgTzAUwCYFoCOBXNKZLAZzQBs0BjGaBAKHowirxDQDsYA6fQ5AMoVqtKAAoEGYADcAxAEMwYBAEo4AenVwAvNoB8cABIAVALIAZACIyAopXZdGzVg558iQyjWgSpcxWBwBgBm0CCy5BAA5sAcqhpaugYmFgBiYXZork4sbJxuBB7C3lAAguTkEiRg8hzcAEbyGFFo8Zo6+nAAchAYaObAJDAA8sEAPCnmAjUcma56TtTk8lBocJTwtXP5AFxGZubbjvRb9vm8hYLFohL1eDC0HLIkePUgwDBtiZ2TAEL3jyOMHoQA)_ 34 | 35 | The example above assumes you're using bundlers or build tools with transpilers, 36 | however, sometimes this may not match your situation. 37 | For example, running `tsc` or Babel out of bundlers. 38 | In this case, you can import this library like this: 39 | 40 | ```typescript 41 | import type {} from 'typed-query-selector' 42 | 43 | document.querySelector('div#app') // ==> HTMLDivElement 44 | ``` 45 | 46 | _[Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBDAnmApnA3gXzgMyhEOAciVQBMBaARwFcUpEKBnFAGxQGMZoiAoXshA40QKAHYwAdLXqIAym07coACiJlgANwDEAQzBgiASjgB6U3AC8lgHxwAEgBUAsgBkAIloCi7URKA)_ 47 | 48 | This looks ugly but it works. 49 | 50 | If you aren't going to use ES Modules you can modify your `tsconfig.json`, 51 | however this is NOT recommended, unless you know what you're doing. 52 | 53 | ```json 54 | { 55 | "compilerOptions": { 56 | "types": ["typed-query-selector"] 57 | } 58 | } 59 | ``` 60 | 61 | ### Strict mode 62 | 63 | > Available in v2.3+ 64 | 65 | In strict mode, the selector parser will perform additional syntax checks on input string. 66 | If there're syntax errors, return type will be `never` instead of `Element`. 67 | 68 | Example usage: 69 | 70 | ```ts 71 | import 'typed-query-selector/strict' 72 | 73 | const element = document.querySelector('div[test') // return `never` 74 | ``` 75 | 76 | This feature won't be enabled by default and you can opt-in. 77 | If you want to enable this, change import entry: 78 | 79 | ```diff 80 | - import 'typed-query-selector' 81 | + import 'typed-query-selector/strict' 82 | ``` 83 | 84 | That's all. If you pass an invalid selector, 85 | because it returns `never`, TypeScript will prevent you from 86 | accessing properties/methods on element or using element at somewhere. 87 | 88 | Note that it doesn't guarantee that it can detect every kind of syntax errors, 89 | since such parser will become very complex and compilation performance may go bad. 90 | 91 | ### Use the parser 92 | 93 | If you just want to use the selector parser itself, we've exported for you: 94 | 95 | ```typescript 96 | import type { 97 | ParseSelector, 98 | StrictlyParseSelector, // or use the strict parser 99 | } from 'typed-query-selector/parser' 100 | 101 | type MyElement = ParseSelector<'form#login'> 102 | ``` 103 | 104 | _[Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBDAnmApnA3nACgQygZxQGUUAbFAYxmjgF84AzKCEOAciVQBMBaARwCuKKIh6FyVaAHoweQlDYAoRZzQBZRAFFyIFADt4AXmxziZStSgAeNg2ggAxKQgBzYHrYA+IA)_ 105 | 106 | Please note that you should import `typed-query-selector/parser`, not `typed-query-selector`. 107 | This is safe because this import doesn't patch to the `querySelector` and `querySelectorAll` function. 108 | 109 | Sometimes, you may want to specify another fallback type (such as `HTMLElement`, not default `Element` type) 110 | when failed to parse selector or failed to look up, you can pass a fallback type as the second type parameter: 111 | 112 | > Available in v2.4+ 113 | 114 | ```ts 115 | import type { ParseSelector } from 'typed-query-selector/parser' 116 | 117 | type MyElement = ParseSelector<'unknown-tag', HTMLElement> // ==> HTMLElement 118 | ``` 119 | 120 | _[Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBDAnmApnA3nACgQygZxQGUUAbFAYxmjgF84AzKCEOAciVQBMBaARwCuKKIh6FyVaAHoweQlDYAoRZzQBZRAFFyIFADt4AXmxziZStSgAeNgL0BrPRADuenjBwBzNgBo4ACQAVNQAZbRRdAwA+OCkpOENDGKDQ8MiYIA)_ 121 | 122 | ## 💡 Supported Use Cases 123 | 124 | ### With class, ID, pseudo class or attribute 125 | 126 | ```typescript 127 | import 'typed-query-selector' 128 | 129 | document.querySelector('div.container') // ==> HTMLDivElement 130 | 131 | document.querySelector('div#app') // ==> HTMLDivElement 132 | 133 | document.querySelector('input[name=username]') // ==> HTMLInputElement 134 | 135 | document.querySelector('input:first-child') // ==> HTMLInputElement 136 | ``` 137 | 138 | _[Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAchgTzAUwCYFoCOBXNKZLAZzQBs0BjGaBAKHowirxDQDsYA6fQ5AMoVqtKAAoEGYADduVCFwCGwDoQQBKOAHotcALx6AfHAASAFQCyAGQAiMgKKV2XRs1bOefIkMo1oEqWkAYkUwMA1tXQNjc2s7aUc0D1cWNk5PAm9hP3EEFTA8GABtDkV2PTwyKFL2AF0InX0jU0srAEkOAphE5KZUj15MwWzRCXzCgC4AM2AoEhgsKgALYHIMBqjm2PbOwp70oA)_ 139 | 140 | Even mix them: 141 | 142 | ```typescript 143 | import 'typed-query-selector' 144 | 145 | document.querySelector('input.form-control[name=username]') // ==> HTMLInputElement 146 | ``` 147 | 148 | _[Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAchgTzAUwCYFoCOBXNKZLAZzQBs0BjGaBAKHowirxDQDsYA6fQ5AMoVqtKAAoEwDmDw8AZtBBYqELlAjkA2hwCG7ALx4yUXewC6CAJRwA9Dbj79APjgAJACoBZADIBJabIAopTsXEA)_ 149 | 150 | And with `:is()` or `:where()`: 151 | 152 | > Available in v2.5+ 153 | 154 | ```typescript 155 | import 'typed-query-selector' 156 | 157 | document.querySelector(':is(div#id, span.class[k=v])') // ==> HTMLDivElement | HTMLSpanElement 158 | 159 | document.querySelector(':where(div#id, span.class[k=v])') // ==> HTMLDivElement | HTMLSpanElement 160 | ``` 161 | 162 | _[Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAchgTzAUwCYFoCOBXNKZLAZzQBs0BjGaBAKHowirxDQDsYA6fQ5AMoVqtKAAoEALmAkxGYADcAxMAwAaOCTABDDtyrltJEgG0A1gF4FAXQCUCW3AD0TuBYsA+OAAkAKgFkAGQARRQBRSnYuOAAfHwDAgR0OCLQomEZmVnTeAiIhShpoCUkAdwALQjQ5RRV1TWT9Q2NzKzsHZ1d3Lz8g0IVU9Nj4oKTdQc4YIA)_ 163 | 164 | ### Combinators 165 | 166 | ```typescript 167 | import 'typed-query-selector' 168 | 169 | document.querySelector('body div') // ==> HTMLDivElement 170 | 171 | document.querySelector('body > form') // ==> HTMLFormElement 172 | 173 | document.querySelector('h1 + p') // ==> HTMLParagraphElement 174 | 175 | document.querySelector('h2 ~ p') // ==> HTMLParagraphElement 176 | ``` 177 | 178 | _[Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAchgTzAUwCYFoCOBXNKZLAZzQBs0BjGaBAKHowirxDQDsYA6fQ5AMoVqtKAAoEAIwgZkcDMABuCAJRwA9OrgBebQD44ACQAqAWQAyAESUBRSuy6NmrBzz5EhlGtAnTZcAwAzaBBVDS1dAxMLADEQuzRXJxY2TjcCD2FvcQQACwBGOABqODAwzR19IzNzAAUAQyh6gHMmsFyEpKYU114MwSzRCVyAJjgAP1LyiKrousaWto77NKA)_ 179 | 180 | ### Grouping selectors 181 | 182 | ```typescript 183 | import 'typed-query-selector' 184 | 185 | document.querySelector('div, span') // ==> HTMLDivElement | HTMLSpanElement 186 | ``` 187 | 188 | _[Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAchgTzAUwCYFoCOBXNKZLAZzQBs0BjGaBAKHowirxDQDsYA6fQ5AMoVqtKAAoEGYADcANHBJgAhhwQBKOAHpNcALy6AfHAASAFQCyAGQAiMgKKV2XOAB8TFywOUcHaJzCA)_ 189 | 190 | ### Fallback 191 | 192 | #### Custom Elements 193 | 194 | If you passed an unknown tag, it will fall back to `Element`. 195 | 196 | ```typescript 197 | import 'typed-query-selector' 198 | 199 | document.querySelector('my-web-component') // ==> Element 200 | ``` 201 | 202 | However, you can override it by specifying a concrete type as a type argument. 203 | 204 | ```typescript 205 | document.querySelector('my-web-component') // ==> MyComponent 206 | ``` 207 | 208 | _[Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAchgTzAUwCYFoCOBXNKZLAZzQBs0BjGaBAKHo2vIEMo04q2SS4BZZAGEI4CADs04+GgAeMKRj4AJACr8AMgFFKIKfADeAX0YYIVPHukA6fIWQBlCtVpQAFAhDEA7mgBGWFSikJLSCACUcAD0UXAAvHEAfHA6aFYwpuaW+rYERE6UNNAAPIIiYqEwiR5eWL4BQRX6EdGxCcllwRL6QA)_ 209 | 210 | Alternatively, you can use [global augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#global-augmentation) and [interface merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces) to extend `HTMLElementTagNameMap` with your custom elements. 211 | 212 | ```typescript 213 | declare global { 214 | interface HTMLElementTagNameMap { 215 | 'my-web-component': MyComponent 216 | } 217 | } 218 | 219 | document.querySelector('my-web-component') // ==> MyComponent 220 | ``` 221 | 222 | _[Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAchgTzAUwCYFoCOBXNKZLAZzQBs0BjGaBAKHo2vIEMo04q2SS4BZZAGEI4CADs04+GgAeMKRj4AJACr8AMgFFKIKfADeAX0bNu7TgHNyEAEatycA-Tiu4waYQBmrKpzWaOmh60qqslgByrHr8rGBOLm5JCCDEAO5otlhUopCS0ggAXALCuRL6ANyJriYmTBBUeCEwAHT4hMgAyhTUtFAAFCnpmdll+TAIAJRwAPQzcAC8CwB8JSJi40A)_ 223 | 224 | #### Invalid selector 225 | 226 | When passing an invalid selector which causes parsing error, 227 | it will fall back to `Element`. 228 | 229 | ```typescript 230 | import 'typed-query-selector' 231 | 232 | document.querySelector('div#app >') // ==> Element 233 | 234 | document.querySelector('div#app ?') // ==> Element 235 | ``` 236 | 237 | However, if you're using strict mode, 238 | all `querySelector` calls above will return `never` type. 239 | This can stop you from misusing it. 240 | 241 | ```ts 242 | import 'typed-query-selector/strict' 243 | 244 | const el = document.querySelector('div#app >') 245 | el.className // TypeScript will report error when compiling 246 | ``` 247 | 248 | ## 🔩 Technical Details 249 | 250 | ### Why returns `never` in strict mode? 251 | 252 | In runtime, if you pass an invalid selector string to `querySelector` or 253 | `querySelectorAll` function, it will throw an error instead of returning 254 | `null` or `undefined` or anything else. 255 | For details, please read [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-never-type). 256 | 257 | ## 🔗 Related 258 | 259 | - [Type Gymnastics](https://github.com/g-plane/type-gymnastics) - Collection of wonderful TypeScript type gymnastics code snippets. 260 | 261 | ## 📃 License 262 | 263 | MIT License 264 | 265 | Copyright (c) 2020-present Pig Fang 266 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typed-query-selector", 3 | "description": "Better typed `querySelector` and `querySelectorAll`.", 4 | "author": "Pig Fang ", 5 | "repository": "g-plane/typed-query-selector", 6 | "version": "2.12.0", 7 | "license": "MIT", 8 | "files": [ 9 | "*.d.ts" 10 | ], 11 | "main": "shim.d.ts", 12 | "types": "shim.d.ts", 13 | "sideEffects": false, 14 | "scripts": { 15 | "test": "tsc -p .", 16 | "fmt": "prettier --write *.ts", 17 | "fmt:check": "prettier --check *.ts" 18 | }, 19 | "devDependencies": { 20 | "@gplane/tsconfig": "^6.1.0", 21 | "@type-challenges/utils": "^0.1.1", 22 | "prettier": "^3.0.3", 23 | "typescript": "^5.5.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /parser.d.ts: -------------------------------------------------------------------------------- 1 | type Whitespace = ' ' | '\n' | '\r' | '\f' | '\t' 2 | type Trim = S extends `${infer T}${Whitespace}` 3 | ? Trim 4 | : S extends `${Whitespace}${infer T}` 5 | ? Trim 6 | : S 7 | 8 | type Combinators = ' ' | '>' | '~' | '+' 9 | type GetLastTag = I extends `${string}${Combinators}${infer Right}` 10 | ? Right extends '' // right arm can't be empty 11 | ? unknown 12 | : GetLastTag 13 | : I 14 | 15 | type PseudoPrefix = 16 | | 'a' 17 | | 'b' 18 | | 'c' 19 | | 'd' 20 | | 'e' 21 | | 'f' 22 | | 'h' 23 | | 'i' 24 | | 'l' 25 | | 'n' 26 | | 'o' 27 | | 'p' 28 | | 'r' 29 | | 's' 30 | | 't' 31 | | 'u' 32 | | 'v' 33 | | 'w' 34 | | ':-' 35 | 36 | type Split = S extends `${string},` // invalid selector 37 | ? unknown 38 | : S extends '' 39 | ? '' 40 | : SplitRec 41 | type SplitRec = S extends `${infer Left},${infer Right}` 42 | ? SplitRec 43 | : S extends '' 44 | ? Acc 45 | : SplitRec<'', Acc | S> 46 | 47 | type Quotes = '"' | "'" 48 | 49 | // DO NOT use union type like `${infer L},${Whitespace}${infer R}` here, 50 | // or it may cause OOM when running tsc in downstream projects. 51 | type PreprocessGrouping = I extends `${infer L}, ${infer R}` 52 | ? PreprocessGrouping<`${L},${R}`> 53 | : I extends `${infer L},\n${infer R}` 54 | ? PreprocessGrouping<`${L},${R}`> 55 | : I extends `${infer L},\r${infer R}` 56 | ? PreprocessGrouping<`${L},${R}`> 57 | : I extends `${infer L},\f${infer R}` 58 | ? PreprocessGrouping<`${L},${R}`> 59 | : I extends `${infer L},\t${infer R}` 60 | ? PreprocessGrouping<`${L},${R}`> 61 | : I 62 | 63 | type Preprocess = I extends `${string}[]${string}` // invalid selector 64 | ? unknown 65 | : PreprocessUnchecked 66 | type PreprocessUnchecked = I extends `${infer L}\\${Quotes}${infer R}` // remove escaped quotes 67 | ? PreprocessUnchecked<`${L}${R}`> 68 | : I extends `${infer L}"${string}"${infer R}` // remove quoted content in attribute 69 | ? PreprocessUnchecked<`${L}${R}`> 70 | : I extends `${infer L}'${string}'${infer R}` // remove quoted content in attribute 71 | ? PreprocessUnchecked<`${L}${R}`> 72 | : I extends `${infer L}[${string}]${infer R}` // process attribute 73 | ? PreprocessUnchecked<`${L}#x${R}`> // replace it with a fake ID selector 74 | : I extends `${infer L}[${string}${infer R}` // process unclosed attribute 75 | ? PreprocessUnchecked<`${L}#x${R}`> // replace it with a fake ID selector 76 | : I 77 | 78 | /** Parse `:is()` and `:where()` */ 79 | type ExpandFunctions = I extends `${infer L}:is(${infer Args})${infer R}` 80 | ? Split> extends string 81 | ? ExpandFunctions<`${L}&${Split>}${R}`> 82 | : unknown 83 | : I extends `${infer L}:where(${infer Args})${infer R}` 84 | ? Split> extends string 85 | ? ExpandFunctions<`${L}&${Split>}${R}`> 86 | : unknown 87 | : I extends `${infer L}:${infer Pseudo}(${string})${infer R}` 88 | ? IsIdentifier extends true 89 | ? ExpandFunctions<`${L}${R}`> 90 | : I 91 | : I 92 | 93 | /** Check whether each tag is valid or not. */ 94 | type Postprocess = PostprocessEach> extends string 95 | ? PostprocessEach> 96 | : unknown 97 | /** Postprocess each tag with simple validation. */ 98 | type PostprocessEach = I extends `${string}.` // invalid class selector 99 | ? unknown 100 | : I extends `${string}#` // invalid ID selector 101 | ? unknown 102 | : PostprocessEachUnchecked 103 | type PostprocessEachUnchecked = 104 | I extends `${infer Tag}.${string}&${infer Rest}` 105 | ? PostprocessEachUnchecked<`${Tag}&${Rest}`> 106 | : I extends `${infer Tag}.${string}` 107 | ? PostprocessEachUnchecked 108 | : I extends `${infer Tag}#${string}&${infer Rest}` 109 | ? PostprocessEachUnchecked<`${Tag}&${Rest}`> 110 | : I extends `${infer Tag}#${string}` 111 | ? PostprocessEachUnchecked 112 | : I extends `${infer Tag}:${PseudoPrefix}${string}&${infer Rest}` 113 | ? PostprocessEachUnchecked<`${Tag}&${Rest}`> 114 | : I extends `${infer Tag}:${PseudoPrefix}${string}` 115 | ? PostprocessEachUnchecked 116 | : I extends `${string}|${infer Tag}` // namespace prefix 117 | ? PostprocessEachUnchecked 118 | : I 119 | 120 | type ParseSelectorToTagNames = Trim extends '' 121 | ? unknown 122 | : Postprocess>>>>> 123 | 124 | export type ParseSelector< 125 | I extends string, 126 | Fallback extends Element = Element, 127 | > = ParseSelectorToTagNames extends string 128 | ? ExpandAnd, Fallback> extends Fallback 129 | ? ExpandAnd, Fallback> 130 | : Fallback 131 | : Fallback 132 | 133 | /** 134 | * Wrapper for `...&...` syntax expander. 135 | * 136 | * `&` is valid, but the expander will return the default result which is `unknown`, 137 | * so we must check the result and if it's `unknown` we will turn it into `Fallback`. 138 | */ 139 | type ExpandAnd< 140 | I extends string, 141 | Fallback extends Element, 142 | > = unknown extends ExpandAndInner 143 | ? Fallback 144 | : ExpandAndInner 145 | 146 | /** 147 | * Actually expand the `...&...` syntax. 148 | * 149 | * The reason why we choose `unknown` as initial type is 150 | * that `unknown & T` equals to `T`. 151 | */ 152 | type ExpandAndInner< 153 | I extends string, 154 | Fallback extends Element, 155 | Result extends Element | unknown = unknown, 156 | > = I extends `${'' | '*'}&${infer Rest}` 157 | ? ExpandAndInner 158 | : I extends `${infer Tag}&${infer Rest}` 159 | ? ExpandAndInner> 160 | : I extends '' | '*' 161 | ? Result 162 | : ExpandAndInner<'', Fallback, Result & TagNameToElement> 163 | 164 | export type TagNameToElement< 165 | Tag extends string, 166 | Fallback extends Element = Element, 167 | > = Tag extends keyof HTMLElementTagNameMap 168 | ? HTMLElementTagNameMap[Tag] 169 | : Tag extends keyof SVGElementTagNameMap 170 | ? SVGElementTagNameMap[Tag] 171 | : Fallback 172 | 173 | // -------------------------------------------------------- 174 | // Strict Parser 175 | // -------------------------------------------------------- 176 | 177 | // Specification is here: https://drafts.csswg.org/css-syntax-3/#ident-token-diagram 178 | // but we don't plan to comply that fully, 179 | // otherwise it will increase type-checking time and the complexity of parser. 180 | 181 | type LowerCaseLetter = 182 | | 'a' 183 | | 'b' 184 | | 'c' 185 | | 'd' 186 | | 'e' 187 | | 'f' 188 | | 'g' 189 | | 'h' 190 | | 'i' 191 | | 'j' 192 | | 'k' 193 | | 'l' 194 | | 'm' 195 | | 'n' 196 | | 'o' 197 | | 'p' 198 | | 'q' 199 | | 'r' 200 | | 's' 201 | | 't' 202 | | 'u' 203 | | 'v' 204 | | 'w' 205 | | 'x' 206 | | 'y' 207 | | 'z' 208 | 209 | type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 210 | 211 | type IdentifierFirstChar = 212 | | LowerCaseLetter 213 | | Uppercase 214 | | '-' 215 | | '_' 216 | type IdentifierChar = IdentifierFirstChar | Digit 217 | 218 | type IsIdentifier = S extends `${infer FirstChar}${infer Rest}` 219 | ? FirstChar extends IdentifierFirstChar 220 | ? IsValidRestChars 221 | : false 222 | : false 223 | 224 | type IsValidTags = S extends `${infer Head}&${infer Rest}` 225 | ? IsValidTagName extends true 226 | ? IsValidTags 227 | : false 228 | : IsValidTagName 229 | type IsValidTagName = S extends '' | '*' 230 | ? true 231 | : S extends `${infer H}${infer Rest}` 232 | ? H extends IdentifierFirstChar 233 | ? IsValidRestChars 234 | : false 235 | : string extends S 236 | ? true 237 | : false 238 | type IsValidRestChars = S extends `${infer H}${infer Rest}` 239 | ? H extends IdentifierChar 240 | ? IsValidRestChars 241 | : false 242 | : true // no characters left, so it's OK 243 | 244 | export type StrictlyParseSelector< 245 | S extends string, 246 | Fallback extends Element = Element, 247 | > = string extends S 248 | ? Fallback 249 | : ParseSelectorToTagNames extends infer Tags 250 | ? IsValidTags extends true 251 | ? Tags extends string 252 | ? ExpandAnd 253 | : never 254 | : never 255 | : never 256 | -------------------------------------------------------------------------------- /parser.test.ts: -------------------------------------------------------------------------------- 1 | import type { Equal, Expect, NotEqual } from '@type-challenges/utils' 2 | import type { ParseSelector } from './parser.js' 3 | 4 | declare class DefinedWebComponent extends HTMLElement { 5 | #brand 6 | } 7 | 8 | declare global { 9 | interface HTMLElementTagNameMap { 10 | 'defined-web-component': DefinedWebComponent 11 | } 12 | } 13 | 14 | type _Tests = [ 15 | Expect, Element>>, 16 | Expect, Element>>, 17 | Expect, Element>>, 18 | Expect, Element>>, 19 | Expect, DefinedWebComponent>>, 20 | Expect, HTMLElement>>, 21 | Expect, Element>>, 22 | Expect, Element>>, 23 | Expect, Element>>, 24 | Expect, HTMLDivElement>>, 25 | Expect, HTMLElement>>, 26 | Expect button'>, HTMLButtonElement>>, 27 | Expect, HTMLParagraphElement>>, 28 | Expect, HTMLParagraphElement>>, 29 | Expect, HTMLHeadingElement>>, 30 | Expect, HTMLHeadingElement>>, 31 | Expect, HTMLHeadingElement>>, 32 | Expect, HTMLHeadingElement>>, 33 | Expect< 34 | Equal< 35 | ParseSelector<` 36 | h2 37 | `>, 38 | HTMLHeadingElement 39 | > 40 | >, 41 | Expect, HTMLDivElement | HTMLSpanElement>>, 42 | Expect, HTMLSpanElement>>, 43 | Expect, HTMLButtonElement>>, 44 | Expect, HTMLInputElement>>, 45 | Expect, HTMLDivElement>>, 46 | Expect'>, Element>>, 47 | Expect, Element>>, 48 | Expect, Element>>, 49 | Expect, Element>>, 50 | Expect< 51 | Equal, HTMLDivElement> 52 | >, 53 | Expect< 54 | Equal< 55 | ParseSelector<` 56 | input, 57 | button.js-comment-cancel-button 58 | `>, 59 | HTMLInputElement | HTMLButtonElement 60 | > 61 | >, 62 | Expect< 63 | Equal< 64 | ParseSelector<`\n input,\n button.js-comment-cancel-button\n `>, 65 | HTMLInputElement | HTMLButtonElement 66 | > 67 | >, 68 | Expect< 69 | Equal< 70 | ParseSelector<'.container > #sign-up-form > div#notice, span.tip'>, 71 | HTMLDivElement | HTMLSpanElement 72 | > 73 | >, 74 | Expect, HTMLDivElement>>, 75 | Expect, HTMLDivElement>>, 76 | Expect, HTMLDivElement>>, 77 | Expect, HTMLDivElement>>, 78 | Expect, HTMLDivElement>>, 79 | Expect, HTMLDivElement>>, 80 | Expect, HTMLDivElement>>, 81 | Expect, HTMLDivElement>>, 82 | Expect, Element>>, 83 | Expect< 84 | Equal, HTMLLinkElement> 85 | >, 86 | Expect< 87 | Equal, HTMLInputElement> 88 | >, 89 | Expect< 90 | Equal, HTMLInputElement> 91 | >, 92 | Expect, HTMLInputElement>>, 93 | Expect, HTMLInputElement>>, 94 | Expect< 95 | Equal, HTMLTextAreaElement> 96 | >, 97 | Expect< 98 | Equal< 99 | ParseSelector<'textarea[name="comment[[[[body]]]]"]'>, 100 | HTMLTextAreaElement 101 | > 102 | >, 103 | Expect< 104 | Equal, HTMLTextAreaElement> 105 | >, 106 | Expect< 107 | Equal, HTMLTextAreaElement> 108 | >, 109 | Expect< 110 | Equal, HTMLTextAreaElement> 111 | >, 112 | Expect, HTMLButtonElement>>, 113 | Expect< 114 | Equal< 115 | ParseSelector<'a[href*="/issues"][href*="is%3Apr"]'>, 116 | HTMLAnchorElement 117 | > 118 | >, 119 | Expect< 120 | Equal< 121 | ParseSelector<` 122 | a[href*="/issues"]:not([href*="sort%3A"]):not(.issues-reset-query), 123 | a[href*="/pulls" ]:not([href*="sort%3A"]):not(.issues-reset-query) 124 | `>, 125 | HTMLAnchorElement 126 | > 127 | >, 128 | Expect, Element>>, 129 | Expect, Element>>, 130 | Expect, Element>>, 131 | Expect, Element>>, 132 | Expect, HTMLDivElement>>, 133 | Expect, HTMLDivElement>>, 134 | Expect, HTMLDivElement>>, 135 | Expect< 136 | Equal< 137 | ParseSelector<'div#test > span, div > button.test'>, 138 | HTMLSpanElement | HTMLButtonElement 139 | > 140 | >, 141 | Expect< 142 | Equal, HTMLDivElement | HTMLSpanElement> 143 | >, 144 | Expect< 145 | Equal< 146 | ParseSelector<':is(div#id, span.cls)'>, 147 | HTMLDivElement | HTMLSpanElement 148 | > 149 | >, 150 | Expect< 151 | Equal< 152 | ParseSelector<':is(div[key], span[key=value])'>, 153 | HTMLDivElement | HTMLSpanElement 154 | > 155 | >, 156 | Expect< 157 | Equal< 158 | ParseSelector<'body :is(div, span)'>, 159 | HTMLDivElement | HTMLSpanElement 160 | > 161 | >, 162 | Expect< 163 | Equal< 164 | ParseSelector<'body :is(div#id, span.cls)'>, 165 | HTMLDivElement | HTMLSpanElement 166 | > 167 | >, 168 | Expect, HTMLParagraphElement>>, 169 | Expect, HTMLParagraphElement>>, 170 | Expect< 171 | Equal, HTMLParagraphElement> 172 | >, 173 | Expect< 174 | Equal< 175 | ParseSelector<'main.container :is(div, span) p[key=value]'>, 176 | HTMLParagraphElement 177 | > 178 | >, 179 | Expect< 180 | Equal, HTMLDivElement | HTMLSpanElement> 181 | >, 182 | Expect< 183 | Equal< 184 | ParseSelector<':where(div#id, span.cls)'>, 185 | HTMLDivElement | HTMLSpanElement 186 | > 187 | >, 188 | Expect< 189 | Equal< 190 | ParseSelector<':where(div[key], span[key=value])'>, 191 | HTMLDivElement | HTMLSpanElement 192 | > 193 | >, 194 | Expect< 195 | Equal< 196 | ParseSelector<'body :where(div, span)'>, 197 | HTMLDivElement | HTMLSpanElement 198 | > 199 | >, 200 | Expect< 201 | Equal< 202 | ParseSelector<'body :where(div#id, span.cls)'>, 203 | HTMLDivElement | HTMLSpanElement 204 | > 205 | >, 206 | Expect, HTMLParagraphElement>>, 207 | Expect< 208 | Equal, HTMLParagraphElement> 209 | >, 210 | Expect< 211 | Equal, HTMLParagraphElement> 212 | >, 213 | Expect< 214 | Equal< 215 | ParseSelector<'main.container :where(div, span) p[key=value]'>, 216 | HTMLParagraphElement 217 | > 218 | >, 219 | Expect< 220 | Equal< 221 | ParseSelector<':is(button.btn, a.btn):not([key=value]):not(.class)'>, 222 | HTMLButtonElement | HTMLAnchorElement 223 | > 224 | >, 225 | Expect< 226 | Equal< 227 | ParseSelector<':is(div.cls, span#id) :is(button.btn, a.btn)'>, 228 | HTMLButtonElement | HTMLAnchorElement 229 | > 230 | >, 231 | Expect< 232 | Equal< 233 | ParseSelector<':is(div.cls, span#id) :where(button.btn, a.btn)'>, 234 | HTMLButtonElement | HTMLAnchorElement 235 | > 236 | >, 237 | Expect< 238 | Equal< 239 | ParseSelector<':where(div.cls, span#id) :is(button.btn, a.btn)'>, 240 | HTMLButtonElement | HTMLAnchorElement 241 | > 242 | >, 243 | Expect< 244 | Equal< 245 | ParseSelector<':where(div.cls, span#id) :where(button.btn, a.btn)'>, 246 | HTMLButtonElement | HTMLAnchorElement 247 | > 248 | >, 249 | Expect< 250 | Equal< 251 | ParseSelector<'p :is(div.cls, span#id) :is(button.btn, a.btn) h1'>, 252 | HTMLHeadingElement 253 | > 254 | >, 255 | Expect, HTMLParagraphElement>>, 256 | Expect, HTMLParagraphElement>>, 257 | Expect< 258 | Equal, HTMLParagraphElement> 259 | >, 260 | Expect< 261 | Equal, HTMLParagraphElement> 262 | >, 263 | Expect, HTMLAnchorElement>>, 264 | Expect< 265 | Equal< 266 | ParseSelector<'a:is([data-selected-links~="pulse"], [data-selected-links~="security"])'>, 267 | HTMLAnchorElement 268 | > 269 | >, 270 | Expect< 271 | Equal< 272 | ParseSelector<'input:is([name="commit_title"], [name="rgh-pr-check-waiter"])'>, 273 | HTMLInputElement 274 | > 275 | >, 276 | Expect img)'>, HTMLAnchorElement>>, 277 | Expect, HTMLHeadingElement>>, 278 | Expect, HTMLElement>>, 279 | Expect, HTMLElement>>, 280 | Expect, HTMLElement>>, 281 | Expect, HTMLElement>>, 282 | Expect, HTMLElement>>, 283 | Expect, HTMLElement>>, 284 | Expect, HTMLElement>>, 285 | Expect, HTMLElement>>, 286 | Expect, HTMLElement>>, 287 | Expect .class', HTMLElement>, HTMLElement>>, 288 | Expect, HTMLElement>>, 289 | Expect :not(a)', HTMLElement>, HTMLElement>>, 290 | Expect< 291 | Equal, HTMLElement> 292 | >, 293 | Expect, HTMLElement>>, 294 | Expect< 295 | Equal< 296 | ParseSelector<':is(a, button).x'>, 297 | HTMLAnchorElement | HTMLButtonElement 298 | > 299 | >, 300 | Expect< 301 | Equal< 302 | ParseSelector<'.x:is(a, button)'>, 303 | HTMLAnchorElement | HTMLButtonElement 304 | > 305 | >, 306 | Expect, HTMLHeadingElement>>, 307 | Expect< 308 | Equal< 309 | ParseSelector<'div:where(.a, .b), .x:is(a, button), h2#x'>, 310 | | HTMLDivElement 311 | | HTMLAnchorElement 312 | | HTMLButtonElement 313 | | HTMLHeadingElement 314 | > 315 | >, 316 | Expect< 317 | Equal< 318 | ParseSelector<`a:is( 319 | [href*="/issues"], [href*="/pulls"], [href*="/projects"], [href*="/labels/"]):not([href*="sort%3A"], .issues-reset-query)`>, 320 | HTMLAnchorElement 321 | > 322 | >, 323 | Expect< 324 | Equal, HTMLAnchorElement> 325 | >, 326 | Expect< 327 | Equal, HTMLAnchorElement> 328 | >, 329 | Expect, HTMLAnchorElement>>, 330 | Expect, HTMLAnchorElement>>, 331 | Expect, HTMLAnchorElement>>, 332 | Expect, HTMLAnchorElement>>, 333 | Expect< 334 | Equal< 335 | ParseSelector<':is(p,button):is(.c)'>, 336 | HTMLParagraphElement | HTMLButtonElement 337 | > 338 | >, 339 | Expect< 340 | Equal< 341 | ParseSelector<':is(.js, .inline) a.author:not([href], [href])'>, 342 | HTMLAnchorElement 343 | > 344 | >, 345 | Expect, Element>>, 346 | Expect< 347 | Equal< 348 | ParseSelector<'.Layout-sidebar :is(.BorderGrid, #partial-discussion-sidebar)'>, 349 | Element 350 | > 351 | >, 352 | Expect< 353 | Equal< 354 | ParseSelector<`#dashboard :is(.watch_started, .fork)${string}`>, 355 | Element 356 | > 357 | >, 358 | Expect, Element>>, 359 | Expect, Element>>, 360 | Expect, HTMLAnchorElement>>, 361 | ] 362 | 363 | declare function delegate< 364 | Selector extends string, 365 | TElement extends Element = ParseSelector, 366 | >(selector: Selector): void 367 | 368 | declare function fetchDom< 369 | Selector extends string, 370 | TElement extends HTMLElement = ParseSelector, 371 | >(selector: Selector): Promise 372 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | '@gplane/tsconfig': 12 | specifier: ^6.1.0 13 | version: 6.1.0 14 | '@type-challenges/utils': 15 | specifier: ^0.1.1 16 | version: 0.1.1 17 | prettier: 18 | specifier: ^3.0.3 19 | version: 3.0.3 20 | typescript: 21 | specifier: ^5.5.3 22 | version: 5.5.3 23 | 24 | packages: 25 | 26 | '@gplane/tsconfig@6.1.0': 27 | resolution: {integrity: sha512-+WNtFH0x1Jjtp5m90dY5/u9wje5GAwjoEHJd60lQUzq9h1vfZ2ILeo9iQjFW9sB06g7mBXDf99iCvY0/t0pAUQ==} 28 | 29 | '@type-challenges/utils@0.1.1': 30 | resolution: {integrity: sha512-A7ljYfBM+FLw+NDyuYvGBJiCEV9c0lPWEAdzfOAkb3JFqfLl0Iv/WhWMMARHiRKlmmiD1g8gz/507yVvHdQUYA==} 31 | 32 | prettier@3.0.3: 33 | resolution: {integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==} 34 | engines: {node: '>=14'} 35 | hasBin: true 36 | 37 | typescript@5.5.3: 38 | resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} 39 | engines: {node: '>=14.17'} 40 | hasBin: true 41 | 42 | snapshots: 43 | 44 | '@gplane/tsconfig@6.1.0': {} 45 | 46 | '@type-challenges/utils@0.1.1': {} 47 | 48 | prettier@3.0.3: {} 49 | 50 | typescript@5.5.3: {} 51 | -------------------------------------------------------------------------------- /shim.d.ts: -------------------------------------------------------------------------------- 1 | import type { ParseSelector } from './parser.js' 2 | 3 | declare global { 4 | interface ParentNode { 5 | querySelector(selector: S): ParseSelector | null 6 | 7 | querySelectorAll( 8 | selector: S, 9 | ): NodeListOf> 10 | } 11 | 12 | interface Element { 13 | closest(selector: S): ParseSelector | null 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /shim.test.ts: -------------------------------------------------------------------------------- 1 | import './shim.js' 2 | import type { Equal, Expect } from '@type-challenges/utils' 3 | 4 | const htmlEl = document.querySelector( 5 | '.container > #sign-up-form > div#notice, span.tip', 6 | ) 7 | type TestEl = Expect< 8 | Equal 9 | > 10 | 11 | declare let selector: string 12 | const link = document.querySelector(selector) 13 | type TestLink = Expect> 14 | 15 | // @ts-expect-error 16 | const a1: HTMLAnchorElement | null = document.querySelector('div') 17 | // @ts-expect-error 18 | const a2: HTMLAnchorElement | null = document.querySelector('div#app') 19 | 20 | const results: Array = [] 21 | // @ts-expect-error 22 | results.push(document.querySelector('div')) 23 | // @ts-expect-error 24 | results.push(document.querySelector('div#app')) 25 | 26 | const closest = htmlEl!.closest('button.btn-confirm, a.link') 27 | type TestClosest = Expect< 28 | Equal 29 | > 30 | 31 | const unknownEl = document.querySelector('unknown') 32 | type TestElement = Expect> 33 | 34 | declare let btn: HTMLButtonElement 35 | const elementOfBtn = btn.querySelector('unknown') 36 | type TestElementOfBtn = Expect> 37 | 38 | declare let element: Element 39 | const elementOfElement = element.querySelector('unknown') 40 | type TestElementOfElement = Expect< 41 | Equal 42 | > 43 | 44 | declare let documentFragment: DocumentFragment 45 | document.querySelector('my-web-component') 46 | document.querySelectorAll('my-web-component') 47 | element.querySelector('my-web-component') 48 | element.querySelectorAll('my-web-component') 49 | documentFragment.querySelector('my-web-component') 50 | documentFragment.querySelectorAll('my-web-component') 51 | -------------------------------------------------------------------------------- /strict.d.ts: -------------------------------------------------------------------------------- 1 | import type { StrictlyParseSelector } from './parser.js' 2 | 3 | declare global { 4 | interface ParentNode { 5 | querySelector>( 6 | selector: S, 7 | ): [E] extends [never] ? never : E | null 8 | 9 | querySelectorAll>( 10 | selector: S, 11 | ): [E] extends [never] ? never : NodeListOf 12 | } 13 | 14 | interface Element { 15 | closest>( 16 | selector: S, 17 | ): [E] extends [never] ? never : E | null 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /strict.test.ts: -------------------------------------------------------------------------------- 1 | import './strict.js' 2 | import type { Equal, Expect } from '@type-challenges/utils' 3 | 4 | const e1 = document.querySelector('#app') 5 | const e2 = document.querySelector('.container') 6 | const e3 = document.querySelector('[data-test]') 7 | const e4 = document.querySelector('[data-test=""]') 8 | const e5 = document.querySelector('#') 9 | const e6 = document.querySelector('.') 10 | const e7 = document.querySelector('[]') 11 | const e8 = document.querySelector('') 12 | const e9 = document.querySelector('*') 13 | const e10 = document.querySelector('li > *') 14 | const e11 = document.querySelector('div#') 15 | const e12 = document.querySelector('div.') 16 | const e13 = document.querySelector('div[]') 17 | const e14 = document.querySelector('div#app') 18 | const e15 = document.querySelector('div.container') 19 | const e16 = document.querySelector('div[data-test]') 20 | const e17 = document.querySelector('div > span') 21 | const e18 = document.querySelector('[') 22 | const e19 = document.querySelector('a6') 23 | const e20 = document.querySelector('a?') 24 | const e21 = document.querySelector('--') 25 | const e22 = document.querySelector('a-b') 26 | const e23 = document.querySelector('a-1-b') 27 | const e24 = document.querySelector('A-1-B') 28 | const e25 = document.querySelector('a_1-B') 29 | const e26 = document.querySelector('div >') 30 | const e27 = document.querySelector('^_^') 31 | const e28 = document.querySelector('div, span') 32 | const e30 = document.querySelector('div, span[') 33 | const e31 = document.querySelector('div,') 34 | const e32 = document.querySelector('div, .badge') 35 | const e33 = document.querySelector('div, .') 36 | const e34 = document.querySelector('') 37 | 38 | const constantSelector = 'p a.lnk' 39 | let variableSelector = 'p a.lnk' 40 | const e35 = document.querySelector(`${constantSelector}`) 41 | const e36 = document.querySelector(`${variableSelector}`) 42 | 43 | declare let element: Element 44 | const e37 = element.querySelector('unknown') 45 | 46 | declare let btn: HTMLButtonElement 47 | const e38 = btn.querySelector('unknown') 48 | 49 | declare let dynamicPart: string 50 | const e39 = document.querySelector(`:is(${dynamicPart}):not(.blob-code-hunk)`) 51 | 52 | type Tests = [ 53 | Expect>, 54 | Expect>, 55 | Expect>, 56 | Expect>, 57 | Expect>, 58 | Expect>, 59 | Expect>, 60 | Expect>, 61 | Expect>, 62 | Expect>, 63 | Expect>, 64 | Expect>, 65 | Expect>, 66 | Expect>, 67 | Expect>, 68 | Expect>, 69 | Expect>, 70 | Expect>, 71 | Expect>, 72 | Expect>, 73 | Expect>, 74 | Expect>, 75 | Expect>, 76 | Expect>, 77 | Expect>, 78 | Expect>, 79 | Expect>, 80 | Expect>, 81 | Expect>, 82 | Expect>, 83 | Expect>, 84 | Expect>, 85 | Expect>, 86 | Expect>, 87 | Expect>, 88 | Expect>, 89 | Expect>, 90 | Expect>, 91 | ] 92 | 93 | // @ts-expect-error 94 | const a1: HTMLAnchorElement | null = document.querySelector('div') 95 | // @ts-expect-error 96 | const a2: HTMLAnchorElement | null = document.querySelector('div#app') 97 | 98 | const results: Array = [] 99 | // @ts-expect-error 100 | results.push(document.querySelector('div')) 101 | // @ts-expect-error 102 | results.push(document.querySelector('div#app')) 103 | 104 | const closest = a1!.closest('button.btn-confirm, a.link') 105 | type TestClosest = Expect< 106 | Equal 107 | > 108 | const invalidClosest = a1!.closest('button.btn-confirm, a.') 109 | type TestInvalidClosest = Expect> 110 | 111 | declare let documentFragment: DocumentFragment 112 | document.querySelector('my-web-component') 113 | document.querySelectorAll('my-web-component') 114 | element.querySelector('my-web-component') 115 | element.querySelectorAll('my-web-component') 116 | documentFragment.querySelector('my-web-component') 117 | documentFragment.querySelectorAll('my-web-component') 118 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@gplane/tsconfig", 3 | "compilerOptions": { 4 | "skipLibCheck": false, 5 | "noEmit": true, 6 | "noUnusedLocals": false 7 | } 8 | } 9 | --------------------------------------------------------------------------------