├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── circle.yml ├── package.json ├── src ├── cli.js ├── index.js ├── index.test.js └── types.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) EGOIST <0x142857@gmail.com> (https://egoist.moe) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

detect-es

3 | 4 |

Detect ESnext features in your code.

5 | 6 |

NPM version NPM downloads CircleCI donate chat

7 | 8 |

9 | preview 10 |

11 | 12 | ## Use cases 13 | 14 | Prevent from shipping unexpected ES6 code to your users, and more... 15 | 16 | ## What's supported? 17 | 18 | ### Features 19 | 20 | - [x] `const` 21 | - [x] `let` 22 | - [x] `template literal` 23 | - [x] `tagged template literal` 24 | - [x] `class` 25 | - [x] `arrow function` 26 | - [x] `async/await` 27 | - [x] `es module` 28 | - [x] `dynamic import` 29 | - [x] `destructuring` 30 | - [x] `generator` 31 | - [x] `for..of` 32 | - [ ] PR to add more... 33 | 34 | ### APIs 35 | 36 | - [x] `Object.assign` 37 | - [x] `Map` `Set` `WeakMap` `WeakSet` 38 | - [x] `Promise` 39 | - [x] `Proxy` 40 | - [ ] PR to add more... 41 | 42 | ## Install 43 | 44 | ```bash 45 | yarn global add detect-es 46 | ``` 47 | 48 | ## CLI 49 | 50 | ```bash 51 | detect-es foo.js 52 | ``` 53 | 54 | ## API 55 | 56 | ```js 57 | const { parse } = require('detect-es') 58 | 59 | const stats = parse('const foo = {...bar}') 60 | console.log(stats.has('const')) 61 | // true 62 | console.log(stats.has('object_rest_spread')) 63 | // true 64 | ``` 65 | 66 | ## Contributing 67 | 68 | 1. Fork it! 69 | 2. Create your feature branch: `git checkout -b my-new-feature` 70 | 3. Commit your changes: `git commit -am 'Add some feature'` 71 | 4. Push to the branch: `git push origin my-new-feature` 72 | 5. Submit a pull request :D 73 | 74 | 75 | ## Author 76 | 77 | **detect-es** © [EGOIST](https://github.com/egoist), Released under the [MIT](./LICENSE) License.
78 | Authored and maintained by EGOIST with help from contributors ([list](https://github.com/egoist/detect-es/contributors)). 79 | 80 | > [egoist.moe](https://egoist.moe) · GitHub [@EGOIST](https://github.com/egoist) · Twitter [@_egoistlily](https://twitter.com/_egoistlily) 81 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/repo 5 | docker: 6 | - image: circleci/node:latest 7 | branches: 8 | ignore: 9 | - gh-pages # list of branches to ignore 10 | - /release\/.*/ # or ignore regexes 11 | steps: 12 | - checkout 13 | - restore_cache: 14 | key: dependency-cache-{{ checksum "yarn.lock" }} 15 | - run: 16 | name: install dependences 17 | command: yarn 18 | - save_cache: 19 | key: dependency-cache-{{ checksum "yarn.lock" }} 20 | paths: 21 | - ./node_modules 22 | - run: 23 | name: test 24 | command: yarn test 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "detect-es", 3 | "version": "0.10.2", 4 | "description": "Detect ES6 features in your code.", 5 | "repository": { 6 | "url": "egoist/detect-es", 7 | "type": "git" 8 | }, 9 | "bin": "dist/cli.js", 10 | "main": "dist/index.js", 11 | "files": [ 12 | "dist" 13 | ], 14 | "scripts": { 15 | "test": "npm run lint && jest", 16 | "lint": "xo", 17 | "build": "bili src/{cli,index}.js --format cjs --filename [name].js --buble.target.node 6", 18 | "watch": "yarn build --watch", 19 | "prepublishOnly": "npm run build" 20 | }, 21 | "author": "egoist <0x142857@gmail.com>", 22 | "license": "MIT", 23 | "dependencies": { 24 | "@babel/traverse": "^7.0.0-beta.36", 25 | "babylon": "^7.0.0-beta.36", 26 | "cac": "^4.3.0", 27 | "chalk": "^2.3.0", 28 | "fs-extra": "^5.0.0", 29 | "globby": "^7.1.1", 30 | "lodash.uniqby": "^4.7.0" 31 | }, 32 | "devDependencies": { 33 | "babel-jest": "^22.0.4", 34 | "babel-preset-env": "^1.6.1", 35 | "bili": "^1.3.0", 36 | "eslint-config-rem": "^3.0.0", 37 | "jest": "^22.0.4", 38 | "xo": "^0.18.0" 39 | }, 40 | "xo": { 41 | "extends": "rem", 42 | "envs": [ 43 | "jest" 44 | ] 45 | }, 46 | "babel": { 47 | "presets": [ 48 | [ 49 | "env", 50 | { 51 | "targets": { 52 | "node": "current" 53 | } 54 | } 55 | ] 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from 'fs-extra' 3 | import cac from 'cac' 4 | import globby from 'globby' 5 | import chalk from 'chalk' 6 | import { parse } from '.' 7 | 8 | const cli = cac() 9 | 10 | cli 11 | .option('apiOnly', { 12 | desc: 'Only show APIs like Promise', 13 | type: 'boolean' 14 | }) 15 | .option('dedupe', { 16 | desc: 'Remove duplicated warnings for the same feature', 17 | type: 'boolean' 18 | }) 19 | 20 | cli.command('*', { 21 | desc: 'Detect from input files', 22 | examples: [ 23 | `${chalk.cyan(cli.bin)} "dist/**/*.js"`, 24 | `${chalk.cyan(cli.bin)} "src/*.js" --dedupe`, 25 | `${chalk.cyan(cli.bin)} "input.js" --apiOnly` 26 | ] 27 | }, async (input, flags) => { 28 | if (input.length === 0) { 29 | return cli.showHelp() 30 | } 31 | const files = await globby(input.concat('!**/node_modules/**')) 32 | if (files.length === 0) { 33 | console.error('No input!') 34 | process.exit(1) 35 | } 36 | const fileStats = await Promise.all( 37 | files.map(file => 38 | fs.readFile(file, 'utf8').then(content => { 39 | const detective = parse(content) 40 | if (flags.apiOnly) { 41 | detective.apiOnly() 42 | } 43 | if (flags.dedupe) { 44 | detective.dedupe() 45 | } 46 | return { stats: detective.stats, file } 47 | }) 48 | ) 49 | ) 50 | for (const fileStat of fileStats) { 51 | if (fileStat.stats.length > 0) { 52 | process.exitCode = 1 53 | } 54 | 55 | for (const stat of fileStat.stats) { 56 | const fileLoc = `${fileStat.file}:${stat.loc.start.line}:${ 57 | stat.loc.start.column 58 | }` 59 | if (stat.type === 'API') { 60 | console.log( 61 | chalk.yellow(stat.type), 62 | 'You may need a polyfill for', 63 | chalk.green(stat.value), 64 | chalk.dim(fileLoc) 65 | ) 66 | } else { 67 | console.log(chalk.cyan(stat.type), chalk.dim(fileLoc)) 68 | } 69 | } 70 | } 71 | }) 72 | 73 | cli.parse() 74 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as babylon from 'babylon' 2 | import traverse from '@babel/traverse' 3 | import uniqBy from 'lodash.uniqby' 4 | import * as types from './types' 5 | 6 | class Detective { 7 | constructor() { 8 | this.stats = [] 9 | } 10 | 11 | has(type) { 12 | return Boolean( 13 | this.stats.find( 14 | item => item.type === type || item.type.toLowerCase() === type 15 | ) 16 | ) 17 | } 18 | 19 | hasAPI(value) { 20 | return Boolean( 21 | this.stats.find(item => item.type === 'API' && item.value === value) 22 | ) 23 | } 24 | 25 | add(item) { 26 | this.stats.push(item) 27 | return this 28 | } 29 | 30 | count(type) { 31 | return this.stats.filter( 32 | item => item.type === type || item.type.toLowerCase() === type 33 | ).length 34 | } 35 | 36 | apiOnly() { 37 | this.stats = this.stats.filter(stat => stat.type === 'API') 38 | return this 39 | } 40 | 41 | dedupe() { 42 | const apiStats = [] 43 | const stats = this.stats.filter(stat => { 44 | if (stat.type === 'API') { 45 | apiStats.push(stat) 46 | return false 47 | } 48 | return true 49 | }) 50 | this.stats = [...uniqBy(stats, 'type'), ...uniqBy(apiStats, 'value')] 51 | return this 52 | } 53 | 54 | hasStats() { 55 | return this.stats.length > 0 56 | } 57 | } 58 | 59 | export function parse(code) { 60 | const detective = new Detective() 61 | const ast = babylon.parse(code, { 62 | sourceType: 'module', 63 | plugins: ['dynamicImport', 'objectRestSpread', 'flow', 'jsx'] 64 | }) 65 | traverse(ast, { 66 | VariableDeclaration(path) { 67 | if (path.node.kind === 'const') { 68 | detective.add({ type: types.CONST, loc: path.node.loc }) 69 | } else if (path.node.kind === 'let') { 70 | detective.add({ type: types.LET, loc: path.node.loc }) 71 | } 72 | }, 73 | TemplateLiteral(path) { 74 | detective.add({ type: types.TEMPLATE_LITERAL, loc: path.node.loc }) 75 | }, 76 | SpreadElement(path) { 77 | detective.add({ type: types.OBJECT_REST_SPREAD, loc: path.node.loc }) 78 | }, 79 | TaggedTemplateExpression(path) { 80 | detective.add({ type: types.TAGGED_TEMPLATE_LITERAL, loc: path.node.loc }) 81 | }, 82 | ClassDeclaration(path) { 83 | detective.add({ type: types.CLASS, loc: path.node.loc }) 84 | }, 85 | ArrowFunctionExpression(path) { 86 | detective.add({ type: types.ARROW_FUNCTION, loc: path.node.loc }) 87 | if (path.node.async) { 88 | detective.add({ type: types.ASYNC, loc: path.node.loc }) 89 | } 90 | }, 91 | FunctionDeclaration(path) { 92 | if (path.node.async) { 93 | detective.add({ type: types.ASYNC, loc: path.node.loc }) 94 | } else if (path.node.generator) { 95 | detective.add({ type: types.GENERATOR, loc: path.node.loc }) 96 | } 97 | }, 98 | ImportDeclaration(path) { 99 | detective.add({ type: types.ES_MODULE, loc: path.node.loc }) 100 | }, 101 | ExportNamedDeclaration(path) { 102 | detective.add({ type: types.ES_MODULE, loc: path.node.loc }) 103 | }, 104 | ExportDefaultDeclaration(path) { 105 | detective.add({ type: types.ES_MODULE, loc: path.node.loc }) 106 | }, 107 | CallExpression(path) { 108 | if (path.get('callee').type === 'Import') { 109 | detective.add({ type: types.DYNAMIC_IMPORT, loc: path.node.loc }) 110 | } 111 | }, 112 | ObjectPattern(path) { 113 | detective.add({ type: types.DESTRUCTURING, loc: path.node.loc }) 114 | }, 115 | ArrayPattern(path) { 116 | detective.add({ type: types.DESTRUCTURING, loc: path.node.loc }) 117 | }, 118 | ObjectMethod(path) { 119 | if (path.node.async) { 120 | detective.add({ type: types.ASYNC, loc: path.node.loc }) 121 | } else if (path.node.generator) { 122 | detective.add({ type: types.GENERATOR, loc: path.node.loc }) 123 | } 124 | }, 125 | ForOfStatement(path) { 126 | detective.add({ type: types.FOR_OF, loc: path.node.loc }) 127 | }, 128 | MemberExpression(path) { 129 | if ( 130 | path.node.object.name === 'Object' && 131 | path.node.property.name === 'assign' 132 | ) { 133 | detective.add({ 134 | type: types.API, 135 | value: 'Object.assign', 136 | loc: path.node.loc 137 | }) 138 | } 139 | }, 140 | // TODO: this is obviously not accurate.. 141 | Identifier(path) { 142 | const nodeNames = ['Promise', 'Proxy', 'Map', 'Set', 'WeakMap', 'WeakSet'] 143 | if (nodeNames.includes(path.node.name)) { 144 | detective.add({ 145 | type: types.API, 146 | value: path.node.name, 147 | loc: path.node.loc 148 | }) 149 | } 150 | } 151 | }) 152 | return detective 153 | } 154 | 155 | export { types } 156 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | import { parse, types } from './' 2 | 3 | test('const', () => { 4 | const detective = parse(`const a = 1`) 5 | expect(detective.has(types.CONST)).toBe(true) 6 | }) 7 | 8 | test('let', () => { 9 | const detective = parse(`let a = 1`) 10 | expect(detective.has(types.LET)).toBe(true) 11 | }) 12 | 13 | test('tagged template literal', () => { 14 | const detective = parse(`let a = foo\`ha\``) 15 | expect(detective.has(types.TAGGED_TEMPLATE_LITERAL)).toBe(true) 16 | }) 17 | 18 | test('class', () => { 19 | const detective = parse(`class Foo {}`) 20 | expect(detective.has(types.CLASS)).toBe(true) 21 | }) 22 | 23 | test('arrow function', () => { 24 | const detective = parse(`const a = () => {}`) 25 | expect(detective.has(types.ARROW_FUNCTION)).toBe(true) 26 | }) 27 | 28 | test('async/await', () => { 29 | expect(parse(`async function foo() {}`).has(types.ASYNC)).toBe(true) 30 | expect(parse(`const foo = async () => {}`).has(types.ASYNC)).toBe(true) 31 | expect(parse(`const foo = {async bar() {}}`).has(types.ASYNC)).toBe(true) 32 | }) 33 | 34 | test('es module', () => { 35 | expect(parse(`import foo from 'foo'`).has(types.ES_MODULE)).toBe(true) 36 | expect(parse(`import {foo} from 'foo'`).has(types.ES_MODULE)).toBe(true) 37 | expect(parse(`export default {}`).has(types.ES_MODULE)).toBe(true) 38 | expect(parse(`export {foo}`).has(types.ES_MODULE)).toBe(true) 39 | expect(parse(`export const foo = {}`).has(types.ES_MODULE)).toBe(true) 40 | }) 41 | 42 | test('dynamic import', () => { 43 | expect(parse(`import('foo')`).has(types.DYNAMIC_IMPORT)).toBe(true) 44 | expect(parse(`foo.import('foo')`).has(types.DYNAMIC_IMPORT)).toBe(false) 45 | }) 46 | 47 | test('destructuring', () => { 48 | expect(parse(`let {a} = {a:1}`).has(types.DESTRUCTURING)).toBe(true) 49 | expect(parse(`let [a] = [1]`).has(types.DESTRUCTURING)).toBe(true) 50 | expect(parse(`function foo({a}) {}`).has(types.DESTRUCTURING)).toBe(true) 51 | }) 52 | 53 | test('generator', () => { 54 | expect(parse(`function * foo() {}`).has(types.GENERATOR)).toBe(true) 55 | expect(parse(`const foo = {*bar() {}}`).has(types.GENERATOR)).toBe(true) 56 | }) 57 | 58 | test('for..of', () => { 59 | expect(parse(`for (const a of b) {}`).has(types.FOR_OF)).toBe(true) 60 | }) 61 | 62 | test('Object.assign', () => { 63 | expect(parse(`Object.assign()`).hasAPI('Object.assign')).toBe(true) 64 | }) 65 | 66 | test('API', () => { 67 | expect(parse(`new Promise()`).hasAPI('Promise')).toBe(true) 68 | expect(parse(`new Proxy()`).hasAPI('Proxy')).toBe(true) 69 | expect(parse(`new Map()`).hasAPI('Map')).toBe(true) 70 | expect(parse(`new WeakMap()`).hasAPI('WeakMap')).toBe(true) 71 | expect(parse(`new Set()`).hasAPI('Set')).toBe(true) 72 | expect(parse(`new WeakSet()`).hasAPI('WeakSet')).toBe(true) 73 | }) 74 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | export const CONST = 'CONST' 2 | export const TEMPLATE_LITERAL = 'TEMPLATE_LITERAL' 3 | export const OBJECT_REST_SPREAD = 'OBJECT_REST_SPREAD' 4 | export const LET = 'LET' 5 | export const TAGGED_TEMPLATE_LITERAL = 'TAGGED_TEMPLATE_LITERAL' 6 | export const CLASS = 'CLASS' 7 | export const ARROW_FUNCTION = 'ARROW_FUNCTION' 8 | export const ASYNC = 'ASYNC' 9 | export const ES_MODULE = 'ES_MODULE' 10 | export const DYNAMIC_IMPORT = 'DYNAMIC_IMPORT' 11 | export const DESTRUCTURING = 'DESTRUCTURING' 12 | export const GENERATOR = 'GENERATOR' 13 | export const FOR_OF = 'FOR_OF' 14 | export const API = 'API' 15 | --------------------------------------------------------------------------------