├── .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 |

7 |
8 |
9 |
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 |
--------------------------------------------------------------------------------