├── .eslintrc ├── .gitignore ├── .npmignore ├── .npmrc ├── LICENSE ├── README.md ├── index.js ├── package.json └── screenshot.png /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "parserOptions": { 4 | "ecmaVersion": 6 5 | }, 6 | "rules": { 7 | "no-bitwise": 0, 8 | "no-console": 0, 9 | "camelcase": 0, 10 | "no-plusplus": 0, 11 | "comma-dangle": 0, 12 | "no-restricted-syntax": 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintrc 2 | README.md 3 | *.tgz 4 | screenshot.png -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Bramus Van Damme - https://www.bram.us/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@bramus/caniuse-cli` 2 | 3 | Command line tool for [“Can I Use …”](https://caniuse.com/) and [MDN Browser Compat Data](https://github.com/mdn/browser-compat-data) 4 | 5 | [![npm](https://img.shields.io/npm/v/%40bramus%2Fcaniuse-cli)](https://www.npmjs.com/package/@bramus/caniuse-cli) 6 | [![NPM](https://img.shields.io/npm/l/%40bramus/caniuse-cli)](./LICENSE) 7 | 8 | ![caniuse-cli screenshot](https://github.com/bramus/caniuse-cli/raw/main/screenshot.png?raw=true) 9 | 10 | ## Features 11 | 12 | * Instant, offline, results powered by [caniuse-db](https://github.com/Fyrd/caniuse) and [`@mdn/browser-compat-data`](https://github.com/mdn/browser-compat-data). 13 | * Collapses versions with the same level of support in the table, just like the [“Can I Use …” website](https://caniuse.com/). 14 | * Shows notes by number. 15 | * Supports tab autocompletion in **zsh**, **bash** and **fish**. 16 | 17 | ## Installation 18 | 19 | ``` 20 | # npm install -g @bramus/caniuse-cli 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```bash 26 | $ caniuse viewport-units 27 | $ caniuse "viewport units" 28 | $ caniuse @property 29 | ``` 30 | 31 | ## Enable Tab Autocompletion 32 | 33 | In **zsh**: 34 | 35 | ```bash 36 | echo '. <(caniuse --completion)' >> ~/.zshrc 37 | ``` 38 | 39 | In **bash**: 40 | 41 | ```bash 42 | caniuse --completion >> ~/.caniuse.completion.sh 43 | echo 'source ~/.caniuse.completion.sh' >> ~/.bashrc 44 | ``` 45 | 46 | In **fish**: 47 | 48 | ```bash 49 | echo 'caniuse --completion-fish | source' >> ~/.config/fish/config.fish 50 | ``` 51 | 52 | That's all! 53 | 54 | Now you have an autocompletion system. 55 | 56 | ## Possible issues 57 | 58 | ### Missing `bash-completion` package 59 | ``` 60 | bash: _get_comp_words_by_ref: command not found 61 | bash: __ltrim_colon_completions: command not found 62 | bash: _get_comp_words_by_ref: command not found 63 | bash: __ltrim_colon_completions: command not found 64 | ``` 65 | 66 | Solution: install `bash-completion` package 67 | 68 | ## License 69 | 70 | `@bramus/caniuse-cli` is released under the MIT public license. See the enclosed `LICENSE` for details. 71 | 72 | ## Acknowledgements 73 | 74 | This project is built on the original https://github.com/dsenkus/caniuse-cli/ -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // TODO: parse markdown links in notes 4 | 5 | const clc = require('cli-color'); 6 | const omelette = require('omelette'); 7 | const wordwrap = require('wordwrap'); 8 | const caniuse = require('caniuse-db/fulldata-json/data-2.0.json'); 9 | const bcd = require('@mdn/browser-compat-data'); 10 | 11 | const wrap = wordwrap(80); 12 | const wrapNote = wordwrap.hard(4, 76); 13 | 14 | const agents = ['chrome', 'edge', 'safari', 'firefox', 'ios_saf', 'and_chr']; 15 | const agents_bcd = ['chrome', 'edge', 'safari', 'firefox', 'safari_ios', 'chrome_android']; 16 | const defaultItemWidth = 10; 17 | 18 | const bcdTitleMap = { 19 | 'api.CSS': 'CSS API', 20 | 'css.at-rules': 'CSS at-rule', 21 | 'css.types': 'CSS Types', 22 | 'css.properties': 'CSS Property', 23 | 'css.selectors': 'CSS Selector', 24 | } 25 | 26 | /** 27 | * getCurrentAgentVersion() returns the current agent version 28 | */ 29 | const getCurrentAgentVersion = function getCurrentAgentVersion(agent) { 30 | try { 31 | return caniuse.agents[agent].current_version; 32 | } catch (error) { 33 | return undefined; 34 | } 35 | }; 36 | 37 | /** 38 | * padCenter() returns fixed length string, 39 | * padding with padStr from both sides if necessary 40 | */ 41 | const padCenter = function padCenter(str, length = defaultItemWidth, padStr = ' ') { 42 | const padLen = ((length - str.length) / 2) + str.length; 43 | return str.padStart(Math.ceil(padLen), padStr).padEnd(length, padStr); 44 | }; 45 | 46 | /** 47 | * printTableHeader() prints `caniuse` table header 48 | */ 49 | const printTableHeader = function printTableHeader(columnWidths) { 50 | agents.forEach((agent) => { 51 | const col = clc.black.bgWhite(padCenter(caniuse.agents[agent].browser, columnWidths[agent], ' ')); 52 | process.stdout.write(col); 53 | process.stdout.write(' '); 54 | }); 55 | 56 | process.stdout.write('\n'); 57 | }; 58 | 59 | /** 60 | * printTableRowItem prints `caniuse` table row column 61 | */ 62 | const printTableRowItem = function printTableRowItem(versionString, statArray, columnWidth) { 63 | const paddedVersionString = padCenter(versionString, columnWidth, ' '); 64 | 65 | // Support is indicated by the first entry in the statArray 66 | switch (statArray[0]) { 67 | case 'y': // (Y)es, supported by default 68 | process.stdout.write(clc.white.bgXterm(28)(paddedVersionString)); 69 | return; 70 | case 'a': // (A)lmost supported (aka Partial support) 71 | process.stdout.write(clc.white.bgXterm(3)(paddedVersionString)); 72 | return; 73 | case 'u': // Support (u)nknown 74 | process.stdout.write(clc.white.bgXterm(240)(paddedVersionString)); 75 | return; 76 | case 'p': // No support, but has (P)olyfill 77 | case 'n': // (N)o support, or disabled by default 78 | case 'x': // Requires prefi(x) to work 79 | case 'd': // (D)isabled by default (need to enable flag or something) 80 | default: 81 | process.stdout.write(clc.white.bgXterm(124)(paddedVersionString)); 82 | } 83 | }; 84 | 85 | /** 86 | * printTableRow prints `caniuse` trable row 87 | */ 88 | const printTableRow = function printTableRow(stats, index, columnWidths) { 89 | agents.forEach((agent, i) => { 90 | const dataItem = stats[agent][index]; 91 | const columnWidth = columnWidths[agent]; 92 | 93 | if (dataItem !== null) { 94 | printTableRowItem(dataItem.versionStringWithNotes, dataItem.statArray, columnWidth); 95 | } else { 96 | // Fill up cell with whitespace 97 | process.stdout.write(padCenter('', columnWidth, ' ')); 98 | } 99 | 100 | // Space between the cells 101 | if (i < agents.length - 1) { 102 | if (dataItem && dataItem.currentVersion) { 103 | process.stdout.write(clc.bgBlackBright(' ')); 104 | } else { 105 | process.stdout.write(' '); 106 | } 107 | } 108 | }); 109 | 110 | process.stdout.write('\n'); 111 | }; 112 | 113 | const prepStats = function prepStats(stats) { 114 | const newStats = {}; 115 | const agentPositions = {}; 116 | const columnWidths = {}; 117 | const allMatchedNotes = new Set(); 118 | 119 | agents.forEach((agent) => { 120 | // Get original stats 121 | // @TODO: handle “all” 122 | const agentStats = stats[agent]; 123 | 124 | // Get current agent version 125 | const currentVersion = getCurrentAgentVersion(agent); 126 | 127 | // Keep track of how many stats we added before the current version, 128 | // after the current version, and where the current version is in the reworked 129 | // set. We use these numbers to align the tables so that there is one row with 130 | // all the current versions 131 | let numBeforeCurrent = 0; 132 | let numAfterCurrent = 0; 133 | let indexOfCurrent = null; 134 | 135 | // Create groups of support 136 | // [ 137 | // { stat: 'n', versions: [1,2,3] }, 138 | // { stat: 'n #1', versions: [4,5,6] }, 139 | // { stat: 'a #2', versions: [7] }, 140 | // { stat: 'y', versions: [8,9,10,11,12] }, 141 | // { stat: 'y', versions: [13] }, <-- Current Version 142 | // { stat: 'y', versions: [14,15,TP] } 143 | // ] 144 | const groupedStats = []; 145 | let prevStat = null; 146 | for (const version_list_entry of caniuse.agents[agent].version_list) { 147 | const { version } = version_list_entry; 148 | const stat = agentStats[version]; 149 | 150 | const isCurrentVersion = (version === currentVersion); 151 | if (stat !== prevStat || isCurrentVersion) { 152 | const statArray = stat.split(' '); 153 | const matchedNotes = stat.split(' ').filter((s) => s.startsWith('#')).map((s) => s.substr(1)); 154 | 155 | groupedStats.push({ 156 | stat, 157 | statArray, 158 | matchedNotes, 159 | versions: [version], 160 | currentVersion: isCurrentVersion, 161 | }); 162 | 163 | matchedNotes.forEach((n) => allMatchedNotes.add(n)); 164 | 165 | if (isCurrentVersion) { 166 | indexOfCurrent = groupedStats.length - 1; 167 | } else if (indexOfCurrent === null) { 168 | numBeforeCurrent++; 169 | } else { 170 | numAfterCurrent++; 171 | } 172 | } else { 173 | groupedStats[groupedStats.length - 1].versions.push(version); 174 | } 175 | 176 | // Store prevStat. Set it to null when isCurrentVersion 177 | // to make sure the currentVersion has its own entry 178 | prevStat = isCurrentVersion ? null : stat; 179 | } 180 | 181 | // Flatten the versions 182 | // E.g. [1,2,3] --> '1-3' 183 | for (const entry of groupedStats) { 184 | const { versions } = entry; 185 | let versionString = ''; 186 | if (versions.length === 1) { 187 | versionString = versions[0]; // eslint-disable-line prefer-destructuring 188 | } else { 189 | const firstVersion = versions[0].split('-')[0]; 190 | const lastVersion = versions[versions.length - 1].includes('-') ? versions[versions.length - 1].split('-')[1] : versions[versions.length - 1]; 191 | versionString = `${firstVersion}-${lastVersion}`; 192 | } 193 | entry.versionString = versionString; 194 | 195 | if (!entry.matchedNotes.length) { 196 | entry.versionStringWithNotes = versionString; 197 | } else { 198 | entry.versionStringWithNotes = `${versionString} [${entry.matchedNotes.join(',')}]`; 199 | } 200 | } 201 | 202 | newStats[agent] = groupedStats; 203 | agentPositions[agent] = { 204 | numBeforeCurrent, 205 | numAfterCurrent, 206 | }; 207 | }); 208 | 209 | // Extract the columnWidth per agent. 210 | // This is derived from the entry with the largest amount of characters 211 | agents.forEach((agent) => { 212 | const stringLengths = newStats[agent].map((a) => a.versionStringWithNotes.length); 213 | const maxStringLength = Math.max( 214 | ...stringLengths, 215 | caniuse.agents[agent].browser.length, 216 | defaultItemWidth 217 | ); 218 | columnWidths[agent] = maxStringLength + 2; 219 | }); 220 | 221 | // Pad the data per agent, so that each agent 222 | // has the same amount of entries before and after the current. 223 | // It’ll result in the current version for each agent being on the same line in the table. 224 | const maxNumBeforeCurrent = Math.max( 225 | ...Object.values(agentPositions) 226 | .map((agentPositionInfo) => agentPositionInfo.numBeforeCurrent) 227 | ); 228 | const maxNumAfterCurrent = Math.max( 229 | ...Object.values(agentPositions) 230 | .map((agentPositionInfo) => agentPositionInfo.numAfterCurrent) 231 | ); 232 | agents.forEach((agent) => { 233 | if (agentPositions[agent].numBeforeCurrent < maxNumBeforeCurrent) { 234 | for (let i = 0; i < maxNumBeforeCurrent - agentPositions[agent].numBeforeCurrent; i++) { 235 | newStats[agent].unshift(null); 236 | } 237 | } 238 | if (agentPositions[agent].numAfterCurrent < maxNumAfterCurrent) { 239 | for (let i = 0; i < maxNumAfterCurrent - agentPositions[agent].numAfterCurrent; i++) { 240 | newStats[agent].push(null); 241 | } 242 | } 243 | }); 244 | 245 | return { 246 | stats: newStats, 247 | numRows: maxNumBeforeCurrent + maxNumAfterCurrent, 248 | matchedNotes: Array.from(allMatchedNotes).sort((a, b) => a - b), 249 | columnWidths, 250 | }; 251 | }; 252 | 253 | /** 254 | * printItem() prints `caniuse` results for specified item 255 | */ 256 | const printItem = function printItem(item) { 257 | const { 258 | stats, numRows, matchedNotes, columnWidths, 259 | } = prepStats(item.stats); 260 | console.log(clc.bold(wrap(`${(item.title ?? '(?)').replaceAll('', '`').replaceAll('', '`')}`))); // @TODO: More HTML to strip? 261 | console.log(clc.underline(`https://caniuse.com/#feat=${item.key}`)); 262 | console.log(); 263 | if (item.description) { 264 | console.log(wrap(item.description)); 265 | console.log(); 266 | } 267 | printTableHeader(columnWidths); 268 | for (let i = 0; i <= numRows; i++) { 269 | printTableRow(stats, i, columnWidths); 270 | } 271 | 272 | if (item.notes) { 273 | console.log(); 274 | console.log(wrap(`Notes: ${item.notes}`)); 275 | } 276 | 277 | if (matchedNotes && matchedNotes.length) { 278 | console.log(); 279 | console.log('Notes by number:'); 280 | console.log(); 281 | matchedNotes.forEach((num) => { 282 | const note = item.notes_by_num[num]; 283 | console.log(wrapNote(`[${num}] ${note}`).trimLeft()); 284 | }); 285 | } 286 | console.log(); 287 | }; 288 | 289 | /** 290 | * parseKeywords() parses keywords from string 291 | * returns parsed array of keywords 292 | */ 293 | const parseKeywords = function parseKeywords(keywords) { 294 | const parsedKeywords = []; 295 | 296 | keywords.split(',') 297 | .map((item) => item.trim()) 298 | .filter((item) => item.length > 0) 299 | .forEach((item) => { 300 | parsedKeywords.push(item.replaceAll(' ', '-')); 301 | }); 302 | 303 | return parsedKeywords; 304 | }; 305 | 306 | const convertBCDSupportToCanIUseStat = function convertBCDSupportToCanIUseStat(agent, bcdSupport) { 307 | let versionSupport = []; 308 | 309 | // Prefill all versions with no support 310 | const allAgentVersions = caniuse.agents[agent].version_list; 311 | for (const agentVersionEntry of allAgentVersions) { 312 | versionSupport.push({ 313 | support: 'x', 314 | version: agentVersionEntry.version, 315 | }); 316 | } 317 | 318 | function process(bcdSupport, versionSupport) { 319 | // There is no BCD available for the agent 320 | if (!bcdSupport) return; 321 | 322 | // This feature was never released if version_added is false 323 | if (bcdSupport.version_added === false) { 324 | return; 325 | } 326 | 327 | // When there are multiple updates, BCD stores it as an array 328 | if (Array.isArray(bcdSupport)) { 329 | bcdSupport.forEach(subEntry => process(subEntry, versionSupport)); 330 | return; 331 | } 332 | 333 | // This feature was released, now determine the start and end offsets in the versions array 334 | let startIndex = 0; 335 | let endIndex = versionSupport.length - 1; 336 | 337 | if (bcdSupport.version_added) { 338 | // Fix for https://github.com/bramus/caniuse-cli/issues/2 339 | // When the version is not found in the list of released versions, color nothing 340 | const matchedIndex = versionSupport.findIndex(e => e.version === bcdSupport.version_added); 341 | if (matchedIndex > -1) { 342 | startIndex = Math.max(startIndex, matchedIndex); 343 | } else { 344 | startIndex = versionSupport.length; 345 | } 346 | } 347 | if (bcdSupport.version_removed) { 348 | endIndex = Math.min(endIndex, versionSupport.findIndex(e => e.version === bcdSupport.version_removed) - 1); 349 | } 350 | 351 | const supportChar = (bcdSupport.partial_implementation === true) ? 'a' : 'y'; 352 | for (let i = startIndex; i <= endIndex; i++) { 353 | versionSupport[i].support = supportChar; 354 | } 355 | 356 | // @TODO: Process prefix, stored in bcdSupport.prefix 357 | // @TODO: Process notes stored in bcdSupport.notes 358 | 359 | return versionSupport; 360 | } 361 | 362 | process(bcdSupport, versionSupport); 363 | 364 | // Return as object 365 | return versionSupport.reduce((toReturn, entry) => { 366 | toReturn[entry.version] = entry.support; 367 | return toReturn; 368 | }, {}); 369 | } 370 | 371 | const convertBCDEntryToCanIUseEntry = function convertBCDEntryToCanIUseEntry(bcdResult) { 372 | const { 373 | key, 374 | origKey, 375 | data, 376 | prefix, 377 | } = bcdResult; 378 | 379 | const toReturn = { 380 | key, 381 | title: `${bcdTitleMap[prefix] ?? `${prefix}`}: ${data.description ?? origKey}`, 382 | description: '', 383 | spec: data.spec_url, 384 | notes: '', 385 | notes_by_num: [], 386 | stats: {}, // To be filled on the next few lines … 387 | }; 388 | 389 | agents_bcd.forEach((agent_bcd, i) => { 390 | // Map BCD agent to CIU agent 391 | const agent_caniuse = agents[i]; 392 | toReturn.stats[agent_caniuse] = convertBCDSupportToCanIUseStat(agent_caniuse, data.support[agent_bcd]); 393 | }); 394 | 395 | return toReturn; 396 | }; 397 | 398 | /** 399 | * findResult() returns `caniuse` item matching given name 400 | */ 401 | const findResult = function findResult(name) { 402 | // Check CanIUse 403 | let caniuseResults = []; 404 | if (caniuse.data[name.replaceAll(' ', '-')] !== undefined) { 405 | caniuseResults = [caniuse.data[name.replaceAll(' ', '-')]]; 406 | } else { 407 | // find caniuse items matching by keyword or firefox_id 408 | caniuseResults = Object.keys(caniuse.data).filter((key) => { 409 | const keywords = parseKeywords(caniuse.data[key].keywords); 410 | 411 | return caniuse.data[key].firefox_id === name 412 | || keywords.includes(name.replaceAll(' ', '-')) 413 | || key.includes(name.replaceAll(' ', '-')) 414 | || caniuse.data[key].title.replaceAll(' ','-').includes(name.replaceAll(' ', '-')) 415 | || keywords.join(',').includes(name.replaceAll(' ', '-')); 416 | }) 417 | .reduce((list, key) => list.concat(caniuse.data[key]), []); 418 | } 419 | 420 | // Check BCD 421 | let bcdResults = []; 422 | for (const section of Object.keys(bcd).filter(k => !k.startsWith('__'))) { // css, js, html, … 423 | for (const [subsectionKey, subsection] of Object.entries(bcd[section])) { // css: at-rules, properties, … 424 | for (const [entryKey, entry] of Object.entries(subsection)) { // properties: writing_mode, word-wrap, … 425 | for (const [subEntryKey, subEntry] of Object.entries(entry)) { // writing-mode: __compat, horizontal-tb, lr, lr-tb, … 426 | if (subEntryKey == '__compat') { 427 | if (entryKey === name || entry['__compat'].description?.includes(name)) { 428 | bcdResults.push({ 429 | key: `mdn-${section}_${subsectionKey}_${entryKey}`, 430 | origKey: entryKey, 431 | data: subEntry, 432 | prefix: `${section}.${subsectionKey}`, 433 | }); 434 | } 435 | } else { 436 | if (subEntryKey === name || subEntry['__compat']?.description?.includes(name)) { 437 | bcdResults.push({ 438 | key: `mdn-${section}_${subsectionKey}_${entryKey}_${subEntryKey}`, 439 | origKey: subEntryKey, 440 | data: subEntry['__compat'], 441 | prefix: `${section}.${subsectionKey}`, 442 | }); 443 | } 444 | } 445 | } 446 | } 447 | } 448 | } 449 | bcdResults = bcdResults.map(bcdResult => convertBCDEntryToCanIUseEntry(bcdResult)); 450 | 451 | // return array of matches 452 | if (caniuseResults.length > 0 || bcdResults.length > 0) { 453 | return [...bcdResults, ...caniuseResults]; 454 | } 455 | 456 | return undefined; 457 | }; 458 | 459 | /** 460 | * omelette tab completion results for first argument 461 | */ 462 | const firstArgument = ({ reply }) => { 463 | // add all keys 464 | const dataKeys = Object.keys(caniuse.data); 465 | 466 | // add keywords and firefox_id's 467 | const otherKeys = Object.keys(caniuse.data).reduce((keys, item) => { 468 | let newKeys = []; 469 | const { firefox_id, keywords } = caniuse.data[item]; 470 | 471 | if (firefox_id && firefox_id.length > 0) { 472 | newKeys.push(firefox_id); 473 | } 474 | 475 | newKeys = newKeys.concat(parseKeywords(keywords)); 476 | 477 | return [].concat(keys, newKeys); 478 | }); 479 | 480 | reply([].concat(dataKeys, otherKeys)); 481 | }; 482 | 483 | // initialize omelette tab completion 484 | omelette`caniuse ${firstArgument}`.init(); 485 | 486 | // inject key for each item in data object 487 | Object.keys(caniuse.data).forEach((key) => { 488 | caniuse.data[key].key = key; 489 | }); 490 | 491 | // find and display result 492 | const name = process.argv[2]; 493 | if (name) { 494 | const res = findResult(name.toLowerCase()); 495 | 496 | if (res !== undefined) { 497 | res.forEach((item) => printItem(item)); 498 | } else { 499 | console.log('Nothing was found'); 500 | } 501 | } else { 502 | console.log('Please pass in an argument, e.g. `caniuse viewport-units`') 503 | } 504 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bramus/caniuse-cli", 3 | "version": "1.1.9", 4 | "description": "Command line tool for “Can I Use…” and MDN Browser Compat Data", 5 | "keywords": [ 6 | "support", 7 | "css", 8 | "js", 9 | "html5", 10 | "svg", 11 | "compat", 12 | "interop" 13 | ], 14 | "main": "index.js", 15 | "bin": { 16 | "caniuse": "./index.js" 17 | }, 18 | "scripts": { 19 | "update-data": "npm update --save caniuse-db @mdn/browser-compat-data && git add package.json && git commit -m \"Update caniuse-db and/or BCD\"", 20 | "lint": "eslint index.js", 21 | "lintfix": "eslint --fix index.js", 22 | "test": "echo \"Error: no test specified\" && exit 1" 23 | }, 24 | "author": "Bramus", 25 | "license": "MIT", 26 | "dependencies": { 27 | "@mdn/browser-compat-data": "^5.6.11", 28 | "caniuse-db": "^1.0.30001675", 29 | "cli-color": "^1.4.0", 30 | "omelette": "^0.4.17", 31 | "wordwrap": "^1.0.0" 32 | }, 33 | "devDependencies": { 34 | "eslint": "^8.57.0", 35 | "eslint-config-airbnb": "^19.0.4", 36 | "eslint-config-airbnb-base": "^15.0.0", 37 | "eslint-plugin-import": "^2.30.0", 38 | "eslint-plugin-jsx-a11y": "^6.10.0", 39 | "eslint-plugin-react": "^7.35.2", 40 | "eslint-plugin-react-hooks": "^4.6.2" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "https://github.com/bramus/caniuse-cli.git" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bramus/caniuse-cli/8f7d3435422eff8d9282600b0a1206ff08e5d323/screenshot.png --------------------------------------------------------------------------------