├── .gitattributes ├── test ├── fake_modules │ ├── empty_file │ │ ├── index.js │ │ └── package.json │ ├── bin_js │ │ ├── .gitignore │ │ ├── package.json │ │ ├── node_modules │ │ │ ├── anybin │ │ │ │ └── package.json │ │ │ └── nobin │ │ │ │ └── package.json │ │ └── index.js │ ├── empty_dep │ │ ├── package.json │ │ └── index.js │ ├── sass │ │ ├── .gitignore │ │ ├── node_modules │ │ │ ├── scss-dep │ │ │ │ └── index.scss │ │ │ └── sass-dep │ │ │ │ └── index.sass │ │ ├── scss.scss │ │ ├── package.json │ │ └── sass.sass │ ├── dev │ │ ├── index.js │ │ └── package.json │ ├── peer_dep │ │ ├── .gitignore │ │ ├── index.js │ │ ├── node_modules │ │ │ └── host │ │ │ │ └── package.json │ │ └── package.json │ ├── require_nothing │ │ ├── index.js │ │ └── package.json │ ├── optional_dep │ │ ├── .gitignore │ │ ├── index.js │ │ ├── node_modules │ │ │ └── host │ │ │ │ └── package.json │ │ └── package.json │ ├── peer_dep_nested │ │ ├── .gitignore │ │ ├── nested │ │ │ └── index.js │ │ ├── node_modules │ │ │ └── host │ │ │ │ └── package.json │ │ └── package.json │ ├── require_resolve_missing │ │ ├── package.json │ │ └── index.js │ ├── import_list_peer │ │ ├── index.txt │ │ ├── node_modules │ │ │ └── direct-dependency │ │ │ │ └── package.json │ │ └── package.json │ ├── import_list │ │ ├── index.txt │ │ └── package.json │ ├── require_resolve │ │ ├── index.js │ │ └── package.json │ ├── bad_deep │ │ ├── test │ │ │ └── sandbox │ │ │ │ └── index.js │ │ └── package.json │ ├── eslint_config │ │ ├── .eslintrc │ │ └── package.json │ ├── missing │ │ ├── package.json │ │ └── index.js │ ├── unreadable_deep │ │ ├── deep │ │ │ └── nested │ │ │ │ └── index.js │ │ └── package.json │ ├── missing_ignore │ │ ├── package.json │ │ └── index.js │ ├── missing_nested │ │ ├── nested │ │ │ ├── package.json │ │ │ └── index.js │ │ ├── index.js │ │ └── package.json │ ├── shebang │ │ ├── index.js │ │ └── package.json │ ├── bad │ │ ├── index.js │ │ └── package.json │ ├── jsx │ │ ├── package.json │ │ └── index.jsx │ ├── good │ │ ├── package.json │ │ └── index.js │ ├── grunt │ │ ├── index.js │ │ └── package.json │ ├── multiple_parsers │ │ ├── index.csv │ │ └── package.json │ ├── bad_js │ │ ├── package.json │ │ └── index.js │ ├── depend │ │ ├── index.js │ │ └── package.json │ ├── nested │ │ ├── package.json │ │ └── index.js │ ├── ignore_number │ │ ├── package.json │ │ └── index.js │ ├── mocha_opts │ │ ├── mocha.opts.txt │ │ └── package.json │ ├── require_dynamic │ │ ├── package.json │ │ └── index.js │ ├── unreadable │ │ └── package.json │ ├── coffee_script │ │ ├── index.coffee │ │ └── package.json │ ├── grunt-tasks │ │ ├── index.js │ │ └── package.json │ ├── good_es7 │ │ ├── package.json │ │ └── index.js │ ├── missing_peer_deps │ │ ├── index.js │ │ └── package.json │ ├── jsx_js │ │ ├── package.json │ │ └── index.js │ ├── express_view_engine │ │ ├── package.json │ │ └── index.js │ ├── typescript │ │ ├── component.tsx │ │ ├── package.json │ │ └── index.ts │ ├── scoped_module │ │ ├── package.json │ │ └── index.js │ ├── good_es6 │ │ ├── package.json │ │ └── index.js │ └── bad_es6 │ │ ├── package.json │ │ └── index.js ├── special │ ├── .gitignore │ ├── node_modules │ │ ├── webpack │ │ │ └── index.js │ │ ├── eslint-config-airbnb │ │ │ ├── base.js │ │ │ ├── react.js │ │ │ └── index.js │ │ ├── eslint-config-preset │ │ │ └── index.js │ │ ├── binary-no-bin │ │ │ └── package.json │ │ ├── binary-no-package │ │ │ └── index.js │ │ └── binary-package │ │ │ └── package.json │ ├── feross-standard.js │ ├── commitizen.js │ ├── mocha.js │ ├── gulp-load-plugins.js │ ├── babel.js │ ├── bin.js │ ├── eslint.js │ └── webpack.js ├── fake_detectors │ ├── exception.js │ └── dependCallExpression.js ├── .eslintrc.yml ├── fake_parsers │ ├── exception.js │ ├── importList.js │ └── multiple.js ├── utils.js ├── cli.js ├── index.js └── spec.js ├── .eslintrc.yml ├── .eslintignore ├── dependent-build.yml ├── src ├── parser │ ├── es6.js │ ├── coffee.js │ ├── jsx.js │ ├── es7.js │ ├── typescript.js │ └── sass.js ├── detector │ ├── importDeclaration.js │ ├── gruntLoadTaskCallExpression.js │ ├── requireCallExpression.js │ ├── expressViewEngine.js │ └── requireResolveCallExpression.js ├── component.json ├── special │ ├── feross-standard.js │ ├── commitizen.js │ ├── mocha.js │ ├── bin.js │ ├── babel.js │ ├── webpack.js │ ├── eslint.js │ └── gulp-load-plugins.js ├── utils │ ├── index.js │ └── get-scripts.js ├── constants.js ├── index.js ├── cli.js └── check.js ├── .editorconfig ├── bin └── depcheck ├── .babelrc ├── .npmignore ├── appveyor.yml ├── false-alert.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── CONTRIBUTING.md ├── package.json ├── README.md └── doc └── pluggable-design.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /test/fake_modules/empty_file/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/special/.gitignore: -------------------------------------------------------------------------------- 1 | !node_modules 2 | -------------------------------------------------------------------------------- /test/fake_modules/bin_js/.gitignore: -------------------------------------------------------------------------------- 1 | !node_modules 2 | -------------------------------------------------------------------------------- /test/fake_modules/empty_dep/package.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /test/fake_modules/sass/.gitignore: -------------------------------------------------------------------------------- 1 | !node_modules 2 | -------------------------------------------------------------------------------- /test/fake_modules/dev/index.js: -------------------------------------------------------------------------------- 1 | require('used-dep'); 2 | -------------------------------------------------------------------------------- /test/fake_modules/peer_dep/.gitignore: -------------------------------------------------------------------------------- 1 | !node_modules 2 | -------------------------------------------------------------------------------- /test/fake_modules/peer_dep/index.js: -------------------------------------------------------------------------------- 1 | require('host'); 2 | -------------------------------------------------------------------------------- /test/fake_modules/require_nothing/index.js: -------------------------------------------------------------------------------- 1 | require(); 2 | -------------------------------------------------------------------------------- /test/fake_modules/optional_dep/.gitignore: -------------------------------------------------------------------------------- 1 | !node_modules 2 | -------------------------------------------------------------------------------- /test/fake_modules/optional_dep/index.js: -------------------------------------------------------------------------------- 1 | require('host'); 2 | -------------------------------------------------------------------------------- /test/fake_modules/peer_dep_nested/.gitignore: -------------------------------------------------------------------------------- 1 | !node_modules 2 | -------------------------------------------------------------------------------- /test/fake_modules/require_resolve_missing/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/fake_modules/import_list_peer/index.txt: -------------------------------------------------------------------------------- 1 | direct-dependency 2 | -------------------------------------------------------------------------------- /test/fake_modules/peer_dep_nested/nested/index.js: -------------------------------------------------------------------------------- 1 | require('host'); 2 | -------------------------------------------------------------------------------- /test/fake_modules/import_list/index.txt: -------------------------------------------------------------------------------- 1 | import-1 2 | import-2 3 | import-3 4 | -------------------------------------------------------------------------------- /test/fake_modules/require_resolve/index.js: -------------------------------------------------------------------------------- 1 | require.resolve('optimist'); 2 | -------------------------------------------------------------------------------- /test/fake_modules/sass/node_modules/scss-dep/index.scss: -------------------------------------------------------------------------------- 1 | $font-size: 1.5em; 2 | -------------------------------------------------------------------------------- /test/special/node_modules/webpack/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 'webpack'; 2 | -------------------------------------------------------------------------------- /test/fake_modules/bad_deep/test/sandbox/index.js: -------------------------------------------------------------------------------- 1 | require('module_bad_deep'); 2 | -------------------------------------------------------------------------------- /test/fake_modules/eslint_config/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "preset" 3 | } 4 | -------------------------------------------------------------------------------- /test/fake_modules/missing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | } 4 | } 5 | -------------------------------------------------------------------------------- /test/fake_modules/require_resolve_missing/index.js: -------------------------------------------------------------------------------- 1 | require.resolve('anyone'); 2 | -------------------------------------------------------------------------------- /test/fake_modules/sass/node_modules/sass-dep/index.sass: -------------------------------------------------------------------------------- 1 | $background-color: red 2 | -------------------------------------------------------------------------------- /test/special/node_modules/eslint-config-airbnb/base.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/special/node_modules/eslint-config-preset/index.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/fake_modules/unreadable_deep/deep/nested/index.js: -------------------------------------------------------------------------------- 1 | require('unreadable-deep'); 2 | -------------------------------------------------------------------------------- /test/fake_modules/missing_ignore/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | } 4 | } 5 | -------------------------------------------------------------------------------- /test/fake_modules/missing_nested/nested/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | } 4 | } 5 | -------------------------------------------------------------------------------- /test/fake_modules/shebang/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('shebang-script'); 4 | -------------------------------------------------------------------------------- /test/special/node_modules/binary-no-bin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "no-binary-entry" 3 | } 4 | -------------------------------------------------------------------------------- /test/fake_modules/bad/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | console.log("I'm bad!"); 4 | -------------------------------------------------------------------------------- /test/fake_modules/bin_js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "anybin": "*" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/jsx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "0.0.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/missing_nested/index.js: -------------------------------------------------------------------------------- 1 | require('used-dep'); 2 | require('outer-missing-dep'); 3 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parser: babel-eslint 2 | extends: airbnb 3 | rules: 4 | import/no-dynamic-require: off 5 | -------------------------------------------------------------------------------- /test/fake_modules/bad/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "optimist": "~0.6.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/good/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "optimist": "~0.6.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/grunt/index.js: -------------------------------------------------------------------------------- 1 | /* global grunt */ 2 | 3 | grunt.loadNpmTasks('grunt-contrib-jshint'); 4 | -------------------------------------------------------------------------------- /test/fake_modules/multiple_parsers/index.csv: -------------------------------------------------------------------------------- 1 | parser_a,a1 2 | parser_a,a2 3 | parser_b,b1 4 | parser_b,b2 5 | -------------------------------------------------------------------------------- /test/fake_detectors/exception.js: -------------------------------------------------------------------------------- 1 | export default (ast) => { 2 | throw ast; // throw anything it gets 3 | }; 4 | -------------------------------------------------------------------------------- /test/fake_modules/bad_js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "optimist": "~0.6.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/bin_js/node_modules/anybin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "bin": { 3 | "any": "bin" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/bin_js/node_modules/nobin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a-package-without-bin-entry" 3 | } 4 | -------------------------------------------------------------------------------- /test/fake_modules/depend/index.js: -------------------------------------------------------------------------------- 1 | /* global depend */ 2 | 3 | depend('depend-1', 'depend-2', 'depend-3'); 4 | -------------------------------------------------------------------------------- /test/fake_modules/nested/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "optimist": "~0.6.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/empty_file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "empty-package": "0.0.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/good/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | const optimist = require('optimist'); 4 | -------------------------------------------------------------------------------- /test/fake_modules/ignore_number/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "number": "1.2.3" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/missing_nested/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "used-dep": "1.2.3" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/mocha_opts/mocha.opts.txt: -------------------------------------------------------------------------------- 1 | --require babel 2 | --require chai 3 | --reporter dot 4 | --ui bdd 5 | -------------------------------------------------------------------------------- /test/fake_modules/require_dynamic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "dynamic": "0.0.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/require_resolve/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies":{ 3 | "optimist": "~0.6.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/unreadable/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "unreadable": "0.0.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/special/node_modules/binary-no-package/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 'export by index.js, without package.json'; 2 | -------------------------------------------------------------------------------- /test/fake_modules/bad_deep/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "module_bad_deep": "~0.0.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/coffee_script/index.coffee: -------------------------------------------------------------------------------- 1 | require 'foo' 2 | require('bar') 3 | require('baz'); 4 | import 'fooo' 5 | -------------------------------------------------------------------------------- /test/fake_modules/grunt-tasks/index.js: -------------------------------------------------------------------------------- 1 | /* global grunt */ 2 | 3 | grunt.tasks.loadNpmTasks('grunt-contrib-jshint'); 4 | -------------------------------------------------------------------------------- /test/fake_modules/grunt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "grunt-contrib-jshint": "~0.6.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore the following file because it is expected to be syntax error 2 | test/fake_modules/bad_js/index.js 3 | -------------------------------------------------------------------------------- /test/fake_modules/empty_dep/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | console.log('I have no dependencies :-('); 4 | -------------------------------------------------------------------------------- /test/fake_modules/good_es7/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "ecmascript-rest-spread": "0.0.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/grunt-tasks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "grunt-contrib-jshint": "~0.6.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/missing_nested/nested/index.js: -------------------------------------------------------------------------------- 1 | require('nested-missing-dep'); // missing dep in nested module is ignored 2 | -------------------------------------------------------------------------------- /test/fake_modules/nested/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | const pkg = require('optimist/package.json'); 4 | -------------------------------------------------------------------------------- /test/fake_modules/require_dynamic/index.js: -------------------------------------------------------------------------------- 1 | /* global a */ 2 | 3 | require('dynamic'); 4 | 5 | require(a.dynamic.call()); 6 | -------------------------------------------------------------------------------- /test/fake_modules/require_nothing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "require-nothing": "0.0.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/sass/scss.scss: -------------------------------------------------------------------------------- 1 | @import "node_modules/scss-dep/index"; 2 | 3 | div { 4 | font-size: $font-size; 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/unreadable_deep/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "unreadable-deep": "0.0.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/special/node_modules/eslint-config-airbnb/react.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'react', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /test/fake_modules/missing_peer_deps/index.js: -------------------------------------------------------------------------------- 1 | require('missing-this-dep'); 2 | require('peer-dep'); 3 | require('optional-dep'); 4 | -------------------------------------------------------------------------------- /test/fake_modules/peer_dep/node_modules/host/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "peerDependencies": { 3 | "peer": "0.0.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/special/node_modules/binary-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "bin": { 3 | "binary-entry": "./bin/binary-exe" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | import/extensions: off 3 | import/no-extraneous-dependencies: off 4 | import/no-unresolved: off 5 | -------------------------------------------------------------------------------- /test/fake_modules/bin_js/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | console.log('The dependency providing `bin` is ignored.'); 4 | -------------------------------------------------------------------------------- /test/fake_modules/jsx_js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "0.0.1", 4 | "jsx-as-js": "0.0.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fake_modules/peer_dep_nested/node_modules/host/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "peerDependencies": { 3 | "peer": "0.0.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/optional_dep/node_modules/host/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "optionalDependencies": { 3 | "optional": "0.0.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_parsers/exception.js: -------------------------------------------------------------------------------- 1 | export default (content) => { 2 | throw new SyntaxError(content); // throws syntax error any way 3 | }; 4 | -------------------------------------------------------------------------------- /test/fake_modules/express_view_engine/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "ejs": "2.5.5", 4 | "express": "4.14.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fake_modules/shebang/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "shebang": "0.0.1", 4 | "shebang-script": "0.0.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fake_modules/express_view_engine/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | app.set('view engine', 'ejs'); 5 | -------------------------------------------------------------------------------- /test/fake_modules/import_list_peer/node_modules/direct-dependency/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "peerDependencies": { 3 | "peer-dependency": "0.0.2" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/missing_ignore/index.js: -------------------------------------------------------------------------------- 1 | require('missing-dep'); 2 | require('missing-ignore-dep'); 3 | require('fs'); // recognize node.js built-in module 4 | -------------------------------------------------------------------------------- /test/fake_modules/peer_dep/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "host": "0.0.1", 4 | "peer": "0.0.1", 5 | "unused-dep": "0.0.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fake_modules/typescript/component.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default class MyComponent extends React.Component<{}, {}> { 4 | } 5 | -------------------------------------------------------------------------------- /test/fake_modules/depend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "depend-1": "0.0.1", 4 | "depend-2": "0.0.2", 5 | "depend-3": "0.0.3" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fake_modules/import_list_peer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "direct-dependency": "0.0.1", 4 | "peer-dependency": "0.0.2" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fake_modules/eslint_config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "eslint-config-preset": "0.0.1", 4 | "eslint-config-unused": "0.0.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fake_modules/import_list/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "import-1": "0.0.1", 4 | "import-2": "0.0.2", 5 | "import-3": "0.0.3" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fake_modules/optional_dep/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "host": "0.0.1", 4 | "optional": "0.0.1", 5 | "unused-dep": "0.0.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fake_modules/sass/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "sass-dep": "0.0.1", 4 | "scss-dep": "0.0.1", 5 | "unused-sass-dep": "0.0.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /dependent-build.yml: -------------------------------------------------------------------------------- 1 | https://github.com/depcheck/depcheck-test-e2e: 2 | - node ${HOST_DIR}/node_modules/dependent-build/patch-package.js 3 | - npm install 4 | - npm test 5 | -------------------------------------------------------------------------------- /test/fake_modules/dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "used-dep": "0.0.1" 4 | }, 5 | "devDependencies": { 6 | "unused-dev-dep": "~1.8.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fake_modules/peer_dep_nested/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "host": "0.0.1", 4 | "peer": "0.0.1", 5 | "unused-nested-dep": "0.0.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/parser/es6.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'babylon'; 2 | 3 | export default function parseES6(content) { 4 | return parse(content, { 5 | sourceType: 'module', 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /test/fake_modules/multiple_parsers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "a1": "0.0.1", 4 | "a2": "0.0.2", 5 | "b1": "0.1.1", 6 | "b2": "0.1.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fake_modules/coffee_script/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "baz": "1.2.3", 4 | "bar": "1.2.3", 5 | "foo": "1.2.3", 6 | "coffee": "1.2.3" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/special/node_modules/eslint-config-airbnb/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint-config-airbnb/base', 4 | 'eslint-config-airbnb/react', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /test/fake_modules/missing_peer_deps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "peerDependencies": { 3 | "peer-dep": "0.0.1" 4 | }, 5 | "optionalDependencies": { 6 | "optional-dep": "0.0.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fake_modules/jsx/index.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | export default React.createClass({ 4 | render() { 5 | return
This is a ES6 JSX syntax file
; 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /test/fake_modules/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "0.0.1", 4 | "ts-dep-1": "0.0.1", 5 | "ts-dep-2": "0.0.1", 6 | "unused-dep": "0.0.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/parser/coffee.js: -------------------------------------------------------------------------------- 1 | import DepsRegex from 'deps-regex'; 2 | 3 | const re = new DepsRegex({ matchES6: false }); 4 | 5 | export default function parseCoffeeScript(content) { 6 | return re.getDependencies(content); 7 | } 8 | -------------------------------------------------------------------------------- /test/fake_modules/missing/index.js: -------------------------------------------------------------------------------- 1 | require('missing-dep'); 2 | require('fs'); // recognize node.js built-in module 3 | 4 | require(1); // ignore require number call 5 | require(); // ignore require call with no arguments 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /test/fake_modules/bad_js/index.js: -------------------------------------------------------------------------------- 1 | // there's a ) missing which will make it impossible parse 2 | ;['util','assert'].forEach(function (thing) { 3 | thing = require('thing') 4 | for (var i in thing) global[i] = thing[i] 5 | } 6 | -------------------------------------------------------------------------------- /test/fake_modules/sass/sass.sass: -------------------------------------------------------------------------------- 1 | @import "node_modules/sass-dep/index" 2 | 3 | // TODO skip the SASS indention syntax now, see sass/node-sass#1192 4 | // body 5 | // display: block 6 | // background-color: $background-color 7 | -------------------------------------------------------------------------------- /src/detector/importDeclaration.js: -------------------------------------------------------------------------------- 1 | export default function detectImportDeclaration(node) { 2 | return node.type === 'ImportDeclaration' && 3 | node.source && 4 | node.source.value 5 | ? [node.source.value] 6 | : []; 7 | } 8 | -------------------------------------------------------------------------------- /test/fake_modules/mocha_opts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "babel": "1.0.0", 4 | "chai": "1.0.0", 5 | "mocha": "1.0.0" 6 | }, 7 | "scripts": { 8 | "test": "mocha --opts mocha.opts.txt" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/fake_modules/typescript/index.ts: -------------------------------------------------------------------------------- 1 | import DepType = require('ts-dep-1'); 2 | import DepClass from 'ts-dep-2'; 3 | 4 | export interface Interface { 5 | dep: DepType; 6 | } 7 | 8 | export class MyClass extends DepClass { 9 | public dep = new DepType(); 10 | } 11 | -------------------------------------------------------------------------------- /bin/depcheck: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-disable no-console, prefer-arrow-callback */ 4 | 5 | require('../dist/cli')( 6 | process.argv.slice(2), 7 | console.log, 8 | console.error, 9 | function exit(code) { 10 | process.exitCode = code; 11 | } 12 | ); 13 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "add-module-exports", 4 | "transform-object-assign" 5 | ], 6 | "presets": [ 7 | "es2015", 8 | "stage-2" 9 | ], 10 | "env": { 11 | "test": { 12 | "plugins": [ 13 | "istanbul" 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/fake_detectors/dependCallExpression.js: -------------------------------------------------------------------------------- 1 | export default function parse(node) { 2 | return node.type === 'CallExpression' && 3 | node.callee && 4 | node.callee.type === 'Identifier' && 5 | node.callee.name === 'depend' 6 | ? node.arguments.map(arg => arg.value) 7 | : []; 8 | } 9 | -------------------------------------------------------------------------------- /test/fake_modules/ignore_number/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Require number is used in minify tools (e.g., browserify, webpack). 3 | * However, number is not a valid package for NPM. 4 | * Reference: https://docs.npmjs.com/files/package.json#name 5 | */ 6 | 7 | require(1); 8 | require(2); 9 | require(3); 10 | -------------------------------------------------------------------------------- /test/fake_modules/scoped_module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@owner/package": "~0.6.0", 4 | "@secondowner/package": "~0.6.0", 5 | "@org/parent": "~0.6.0", 6 | "@unused/package": "~0.6.0", 7 | "name-import": "1.2.3", 8 | "child-import": "1.2.3" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/parser/jsx.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'babylon'; 2 | 3 | export default function parseJSX(content) { 4 | return parse(content, { 5 | sourceType: 'module', 6 | 7 | // Enable all possible babylon plugins. 8 | // Because the guys using React always want the newest syntax. 9 | plugins: ['*', 'jsx'], 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/parser/es7.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'babylon'; 2 | 3 | export default function parseES7(content) { 4 | return parse(content, { 5 | sourceType: 'module', 6 | 7 | // Enable all possible babylon plugins. 8 | // Because we only parse them, not evaluate any code, it is safe to do so. 9 | plugins: ['*'], 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/detector/gruntLoadTaskCallExpression.js: -------------------------------------------------------------------------------- 1 | export default function detectGruntLoadTaskCallExpression(node) { 2 | return node.type === 'CallExpression' && 3 | node.callee && 4 | node.callee.property && 5 | node.callee.property.name === 'loadNpmTasks' && 6 | node.arguments[0] 7 | ? [node.arguments[0].value] 8 | : []; 9 | } 10 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | // eslint-disable-next-line import/prefer-default-export 4 | export function resolveShortPath(expected, module) { 5 | return Object.keys(expected).reduce((obj, key) => ({ 6 | ...obj, 7 | [key]: expected[key].map(name => 8 | path.resolve(__dirname, 'fake_modules', module, name)), 9 | }), {}); 10 | } 11 | -------------------------------------------------------------------------------- /test/fake_modules/jsx_js/index.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const jsx = require('jsx-as-js'); 3 | 4 | // eslint-disable-next-line react/prefer-es6-class, react/prefer-stateless-function 5 | export default React.createClass({ 6 | render() { 7 | // eslint-disable-next-line react/jsx-filename-extension 8 | return
Enable {jsx} syntax in js file by default.
; 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # This file is to avoid `npm install` encounter chmod ENOENT error. 2 | # Reference: http://stackoverflow.com/questions/17990647/npm-install-errors-with-error-enoent-chmod 3 | 4 | # Ignore there folders because they will not be used in production code. 5 | # Reference: http://blog.xebia.com/2015/09/22/publishing-es6-code-to-npm/ 6 | doc/ 7 | src/ 8 | test/ 9 | build/ 10 | coverage/ 11 | .nyc_output 12 | -------------------------------------------------------------------------------- /src/detector/requireCallExpression.js: -------------------------------------------------------------------------------- 1 | import lodash from 'lodash'; 2 | 3 | export default function requireCallExpression(node) { 4 | return node.type === 'CallExpression' && 5 | node.callee && 6 | node.callee.type === 'Identifier' && 7 | node.callee.name === 'require' && 8 | node.arguments[0] && 9 | lodash.isString(node.arguments[0].value) 10 | ? [node.arguments[0].value] 11 | : []; 12 | } 13 | -------------------------------------------------------------------------------- /test/fake_parsers/importList.js: -------------------------------------------------------------------------------- 1 | function toRequire(dep) { 2 | return { 3 | type: 'ImportDeclaration', 4 | source: { 5 | type: 'Literal', 6 | value: dep, 7 | }, 8 | }; 9 | } 10 | 11 | export function lite(content) { 12 | return content.replace(/\r\n/g, '\n').split('\n').filter(line => line); 13 | } 14 | 15 | export function full(content) { 16 | return lite(content).map(toRequire); 17 | } 18 | -------------------------------------------------------------------------------- /test/fake_modules/good_es7/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | // This file includes some experimental ES7 syntax enabled in Babel by default. Reference: https://babeljs.io/docs/usage/experimental/ 4 | 5 | import 'ecmascript-rest-spread'; 6 | 7 | const test = { 8 | x: 1, 9 | y: 2, 10 | z: 3, 11 | }; 12 | 13 | const objectRestSpread = { 14 | ...test, 15 | spec: 'https://github.com/sebmarkbage/ecmascript-rest-spread', 16 | }; 17 | -------------------------------------------------------------------------------- /src/detector/expressViewEngine.js: -------------------------------------------------------------------------------- 1 | import lodash from 'lodash'; 2 | 3 | export default function expressViewEngine(node) { 4 | return node.type === 'CallExpression' && 5 | node.callee && 6 | node.callee.property && 7 | node.callee.property.name === 'set' && 8 | node.arguments[0] && 9 | node.arguments[0].value === 'view engine' && 10 | node.arguments[1] && 11 | lodash.isString(node.arguments[1].value) 12 | ? [node.arguments[1].value] 13 | : []; 14 | } 15 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: 4 4 | - nodejs_version: 6 5 | - nodejs_version: 8 6 | 7 | install: 8 | - ps: Install-Product node $env:nodejs_version 9 | - npm -g install npm@3 && set PATH=%APPDATA%\npm;%PATH% 10 | - npm install 11 | 12 | test_script: 13 | - node --version 14 | - npm --version 15 | - npm run compile 16 | - npm run component 17 | - npm run depcheck 18 | - npm run depcheck-json 19 | - npm run lint 20 | - npm test 21 | 22 | build: off 23 | -------------------------------------------------------------------------------- /test/fake_modules/good_es6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "basic-import": "1.2.3", 4 | "name-import": "1.2.3", 5 | "star-import": "1.2.3", 6 | "member-import": "1.2.3", 7 | "member-alias-import": "1.2.3", 8 | "multiple-member-import": "1.2.3", 9 | "mixed-member-alias-import": "1.2.3", 10 | "mixed-name-memeber-import": "1.2.3", 11 | "mixed-default-star-import": "1.2.3", 12 | "default-member-import": "1.2.3", 13 | "unsupported-syntax": "1.2.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/fake_modules/scoped_module/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | /** 4 | * This covers various ways module names can be found in require statements. 5 | * Module names are extracted using https://github.com/mattdesl/require-package-name 6 | */ 7 | 8 | const pkg = require('@owner/package'); 9 | const anotherPackage = require('@secondowner/package'); 10 | const childDep = require('@org/parent/child'); 11 | const name = require('name-import/name'); 12 | const deepName = require('child-import/deep/name'); 13 | -------------------------------------------------------------------------------- /test/fake_modules/bad_es6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "dont-find-me": "~0.0.1", 4 | "find-me": "^0.1.0" 5 | }, 6 | "devDependencies": { 7 | "name-import": "1.2.3", 8 | "star-import": "1.2.3", 9 | "member-import": "1.2.3", 10 | "member-alias-import": "1.2.3", 11 | "multiple-member-import": "1.2.3", 12 | "mixed-member-alias-import": "1.2.3", 13 | "mixed-name-memeber-import": "1.2.3", 14 | "mixed-default-star-import": "1.2.3", 15 | "default-member-import": "1.2.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "notice": "This file is for debugging and testing only, the production one is built from `build/component.js` script.", 3 | "parser": [ 4 | "coffee", 5 | "es6", 6 | "es7", 7 | "jsx", 8 | "sass", 9 | "typescript" 10 | ], 11 | "detector": [ 12 | "expressViewEngine", 13 | "gruntLoadTaskCallExpression", 14 | "importDeclaration", 15 | "requireCallExpression", 16 | "requireResolveCallExpression" 17 | ], 18 | "special": [ 19 | "eslint", 20 | "mocha" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /test/fake_parsers/multiple.js: -------------------------------------------------------------------------------- 1 | function parser(prefix, content) { 2 | return content 3 | .replace(/\r\n/g, '\n') 4 | .split('\n') 5 | .filter(line => line.startsWith(prefix)) 6 | .map(line => ({ 7 | type: 'ImportDeclaration', 8 | source: { 9 | type: 'Literal', 10 | value: line.substring(prefix.length), 11 | }, 12 | })); 13 | } 14 | 15 | export const multipleParserA = content => 16 | parser('parser_a,', content); 17 | 18 | export const multipleParserB = content => 19 | parser('parser_b,', content); 20 | -------------------------------------------------------------------------------- /src/detector/requireResolveCallExpression.js: -------------------------------------------------------------------------------- 1 | import lodash from 'lodash'; 2 | 3 | export default function detectRequireCallExpression(node) { 4 | return node.type === 'CallExpression' && 5 | node.callee && 6 | node.callee.type === 'MemberExpression' && 7 | node.callee.object && 8 | node.callee.object.name === 'require' && 9 | node.callee.property && 10 | node.callee.property.name === 'resolve' && 11 | node.arguments[0] && 12 | lodash.isString(node.arguments[0].value) 13 | ? [node.arguments[0].value] 14 | : []; 15 | } 16 | -------------------------------------------------------------------------------- /src/special/feross-standard.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default function parseFerossStandard(content, filePath, deps, rootDir) { 4 | const packageJsonPath = path.resolve(rootDir, 'package.json'); 5 | const resolvedFilePath = path.resolve(filePath); 6 | if (resolvedFilePath === packageJsonPath && deps.indexOf('standard') !== -1) { 7 | const metadata = JSON.parse(content); 8 | const config = metadata.standard || {}; 9 | const parser = config.parser; 10 | return parser ? [parser] : []; 11 | } 12 | 13 | return []; 14 | } 15 | -------------------------------------------------------------------------------- /false-alert.js: -------------------------------------------------------------------------------- 1 | /** 2 | * I am using this file to avoid false alert in this project. 3 | * It is not good, however it works. 4 | * The typescript and node-sass are parsers as peer dependencies. 5 | * However, because of NPM's bug, it is blocking users. 6 | * They are only mentioned in README and declared in devDependencies for testing. 7 | * Reference: https://github.com/depcheck/depcheck/issues/130 8 | */ 9 | import 'node-sass'; 10 | import 'typescript'; 11 | 12 | /** 13 | * Recongnize the required module by nyc. See depcheck/depcheck#183 14 | */ 15 | import 'babel-polyfill'; 16 | import 'babel-register'; 17 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import vm from 'vm'; 2 | 3 | export { default as getScripts } from './get-scripts'; 4 | 5 | export function readJSON(filePath) { 6 | return require(filePath); // eslint-disable-line global-require 7 | } 8 | 9 | export function evaluate(code) { 10 | const exports = {}; 11 | const sandbox = { 12 | exports, 13 | module: { exports }, 14 | }; 15 | 16 | vm.runInNewContext(code, sandbox); 17 | 18 | return sandbox.module.exports; 19 | } 20 | 21 | export function tryRequire(module) { 22 | try { 23 | return require(module); // eslint-disable-line global-require 24 | } catch (e) { 25 | return null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/fake_modules/bad_es6/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars, id-length */ 2 | 3 | /** 4 | * This should cover all styles of ES6 imports as described by: 5 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import 6 | */ 7 | 8 | import 'find-me'; 9 | import name from 'name-import'; 10 | import * as star from 'star-import'; 11 | import { member } from 'member-import'; 12 | import { foobar as barfoo } from 'member-alias-import'; 13 | import { foo, bar } from 'multiple-member-import'; 14 | import { a, b as c } from 'mixed-member-alias-import'; 15 | import d, { e } from 'mixed-name-memeber-import'; 16 | import h, * as i from 'mixed-default-star-import'; 17 | import j from 'default-member-import'; 18 | -------------------------------------------------------------------------------- /test/special/feross-standard.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | import 'should'; 4 | import standardSpecialParser from '../../src/special/feross-standard'; 5 | 6 | describe('feross standard special parser', () => { 7 | it('should ignore when it is not `package.json`', () => { 8 | const result = standardSpecialParser('content', '/a/file', ['standard'], '/a'); 9 | result.should.deepEqual([]); 10 | }); 11 | 12 | it('should recognize the parser used by feross standard', () => { 13 | const metadata = { 14 | standard: { 15 | parser: 'babel-eslint', 16 | }, 17 | }; 18 | 19 | const content = JSON.stringify(metadata); 20 | const result = standardSpecialParser(content, '/a/package.json', ['standard'], '/a'); 21 | result.should.deepEqual(['babel-eslint']); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/parser/typescript.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'babylon'; 2 | import { tryRequire } from '../utils'; 3 | 4 | const typescript = tryRequire('typescript'); 5 | 6 | export default function parseTypescript(content, filePath) { 7 | if (!typescript) { 8 | return []; 9 | } 10 | 11 | const compileOptions = { 12 | module: typescript.ModuleKind.CommonJS, 13 | target: typescript.ScriptTarget.Latest, 14 | }; 15 | 16 | const result = typescript.transpile( 17 | content, 18 | compileOptions, 19 | filePath); 20 | 21 | // TODO avoid parse source file twice, use Typescript native traverser to find out dependencies. 22 | // Reference: https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#traversing-the-ast-with-a-little-linter 23 | return parse(result, { 24 | sourceType: 'module', 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 28 | node_modules 29 | 30 | # Babel compiled code 31 | dist 32 | 33 | # Nyc output 34 | .nyc_output 35 | 36 | # Vim temporary file 37 | *.swp 38 | 39 | # Dependent-build 40 | dependent-build 41 | 42 | # IDE files 43 | .idea/ 44 | *.iml 45 | -------------------------------------------------------------------------------- /src/parser/sass.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import lodash from 'lodash'; 3 | import requirePackageName from 'require-package-name'; 4 | import { tryRequire } from '../utils'; 5 | 6 | const sass = tryRequire('node-sass'); 7 | 8 | export default function parseSASS(content, filePath, deps, rootDir) { 9 | const { stats } = sass.renderSync({ 10 | data: content, 11 | includePaths: [path.dirname(filePath)], 12 | }); 13 | 14 | const result = lodash(stats.includedFiles) 15 | .map(file => path.relative(rootDir, file)) 16 | .filter(file => file.indexOf('node_modules') === 0) // refer to node_modules 17 | .map(file => file.replace(/\\/g, '/')) // normalize paths in Windows 18 | .map(file => file.substring('node_modules/'.length)) // avoid heading slash 19 | .map(requirePackageName) 20 | .uniq() 21 | .value(); 22 | 23 | return result; 24 | } 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - 4 7 | - 6 8 | - 8 9 | 10 | script: 11 | - npm run compile 12 | - npm run component 13 | - npm run depcheck 14 | - npm run depcheck-json 15 | - npm run lint 16 | - npm run test-coverage 17 | - npm run test-dependent 18 | - cat ./coverage/coverage.lcov | ./node_modules/.bin/codecov 19 | 20 | before_deploy: 21 | - ./node_modules/.bin/patch-version 22 | 23 | deploy: 24 | - provider: npm 25 | skip_cleanup: true 26 | email: lijunle@gmail.com 27 | api_key: 28 | secure: dEwnWZyvE95Amn3Ivcftk9wlDnRrNS2roVXREamsEJI3Rdilim7iHsINeVW/hZIv6VGepETKdSZ+nbyY/Du4rFkMyimHFBM9KkHgw46J7cf3juwZChzna/qOY+DniwGgikFscL+XAuKNx6Rv8xX4dD28TNoYzL00AtDhHJKla3E= 29 | on: 30 | tags: true 31 | node: 6 32 | - provider: releases 33 | skip_cleanup: true 34 | api_key: 35 | secure: Uiilnj7sn8+KEgCMOByMdvCVARfAhvZGUvVrP1oijJBzrMr/YV5lPjH2Hph5IaJuvKKKagXXxqmcspkeiT/kaQA9TVg7VYh+B9DqhKiDxAkBYwMLgiPmnqDTh5ZGNOnnltVxhBI/qRDXpTj1i92VvloVlCcKQ3/M5mqlJi0j+8w= 36 | on: 37 | tags: true 38 | node: 6 39 | -------------------------------------------------------------------------------- /src/special/commitizen.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import requirePackageName from 'require-package-name'; 3 | 4 | export default function parseCommitizen(content, filePath, deps, rootDir) { 5 | const packageJsonPath = path.resolve(rootDir, 'package.json'); 6 | const resolvedFilePath = path.resolve(filePath); 7 | 8 | if (resolvedFilePath === packageJsonPath) { 9 | const metadata = JSON.parse(content); 10 | 11 | if (metadata.config && metadata.config.commitizen && metadata.config.commitizen.path) { 12 | const commitizenPath = metadata.config.commitizen.path; 13 | 14 | if (!commitizenPath.startsWith('.')) { 15 | return [requirePackageName(commitizenPath)]; 16 | } 17 | 18 | const normalizedPath = path.normalize(commitizenPath).replace(/\\/g, '/'); 19 | 20 | if (!normalizedPath.startsWith('node_modules')) { 21 | // The path is not refering to a file in another module 22 | return []; 23 | } 24 | 25 | const packagePath = normalizedPath.substring('node_modules/'.length); 26 | 27 | return [requirePackageName(packagePath)]; 28 | } 29 | } 30 | 31 | return []; 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2015 Djordje Lukic, Junle Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/fake_modules/good_es6/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars, id-length */ 2 | 3 | /** 4 | * This should cover (nearly) all ES6 import syntaxes as described by: 5 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import 6 | */ 7 | 8 | import 'basic-import'; 9 | import name from 'name-import'; 10 | import * as star from 'star-import'; 11 | import { member } from 'member-import'; 12 | import { foobar as barfoo } from 'member-alias-import'; 13 | import { foo, bar } from 'multiple-member-import'; 14 | import { a, b as c } from 'mixed-member-alias-import'; 15 | import d, { e } from 'mixed-name-memeber-import'; 16 | import h, * as i from 'mixed-default-star-import'; 17 | import j from 'default-member-import'; 18 | 19 | // import 'unsupportedSyntax' as seeBelow; 20 | /* 21 | * The import syntax shown on MDN as `import 'module-name' as name;` is 22 | * currently unsupported. 23 | * 24 | * This is due to it being unsupported by the JavaScript parsing libraries that 25 | * we use. 26 | * 27 | * Additionally, this syntax is not supported by Babel (currently), so we felt 28 | * that it was reasonable to not support it at this time. We've left this here 29 | * for future reference. 30 | * 31 | * https://github.com/lijunle/depcheck-es6/pull/7 32 | */ 33 | -------------------------------------------------------------------------------- /src/utils/get-scripts.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import yaml from 'js-yaml'; 4 | import lodash from 'lodash'; 5 | 6 | const scriptCache = {}; 7 | 8 | function getCacheOrFile(key, fn) { 9 | if (scriptCache[key]) { 10 | return scriptCache[key]; 11 | } 12 | 13 | const value = fn(); 14 | scriptCache[key] = value; 15 | 16 | return value; 17 | } 18 | 19 | const travisCommands = [ 20 | // Reference: http://docs.travis-ci.com/user/customizing-the-build/#The-Build-Lifecycle 21 | 'before_install', 22 | 'install', 23 | 'before_script', 24 | 'script', 25 | 'after_success or after_failure', 26 | 'before_deploy', 27 | 'after_deploy', 28 | 'after_script', 29 | ]; 30 | 31 | export default function getScripts(filepath, content = null) { 32 | return getCacheOrFile(filepath, () => { 33 | const basename = path.basename(filepath); 34 | const fileContent = content !== null ? content : fs.readFileSync(filepath, 'utf-8'); 35 | 36 | if (basename === 'package.json') { 37 | return lodash.values(JSON.parse(fileContent).scripts || {}); 38 | } else if (basename === '.travis.yml') { 39 | const metadata = yaml.safeLoad(content) || {}; 40 | return lodash(travisCommands) 41 | .map(cmd => metadata[cmd] || []) 42 | .flatten() 43 | .value(); 44 | } 45 | 46 | return []; 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/special/mocha.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import lodash from 'lodash'; 4 | import requirePackageName from 'require-package-name'; 5 | import { getScripts } from '../utils'; 6 | 7 | function getOpts(script) { 8 | const argvs = script.split(' ').filter(argv => argv); 9 | const optsIndex = argvs.indexOf('--opts'); 10 | return optsIndex !== -1 ? argvs[optsIndex + 1] : null; 11 | } 12 | 13 | function getRequires(content, deps) { 14 | return content 15 | .split('\n') 16 | .map(line => line.trim()) 17 | .filter(line => line.indexOf('--require ') === 0) 18 | .map(line => line.substring('--require '.length).trim()) 19 | .map(requirePackageName) 20 | .filter(name => deps.indexOf(name) !== -1); 21 | } 22 | 23 | export default function parseMocha(content, filepath, deps, rootDir) { 24 | const defaultOptPath = path.resolve(rootDir, 'test/mocha.opts'); 25 | if (filepath === defaultOptPath) { 26 | return getRequires(content, deps); 27 | } 28 | 29 | // get mocha.opts from scripts 30 | const requires = lodash(getScripts(filepath, content)) 31 | .filter(script => script.indexOf('mocha') !== -1) 32 | .map(script => getOpts(script)) 33 | .filter(opts => opts) 34 | .map(opts => path.resolve(filepath, '..', opts)) 35 | .map(optPath => fs.readFileSync(optPath, 'utf-8')) // TODO async read file 36 | .map(optContent => getRequires(optContent, deps)) 37 | .flatten() 38 | .value(); 39 | 40 | return requires; 41 | } 42 | -------------------------------------------------------------------------------- /test/special/commitizen.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | import 'should'; 4 | import commitizenSpecialParser from '../../src/special/commitizen'; 5 | 6 | describe('commitizen special parser', () => { 7 | it('should ignore when it is not `package.json`', () => { 8 | const result = commitizenSpecialParser('content', '/a/file', [], '/a'); 9 | result.should.deepEqual([]); 10 | }); 11 | 12 | it('should ignore when path is missing', () => { 13 | const metadata = { 14 | config: { 15 | commitizen: {}, 16 | }, 17 | }; 18 | 19 | const content = JSON.stringify(metadata); 20 | const result = commitizenSpecialParser(content, '/a/package.json', [], '/a'); 21 | result.should.deepEqual([]); 22 | }); 23 | 24 | it('should recognize the module used by commitizen (long style)', () => { 25 | const metadata = { 26 | config: { 27 | commitizen: { 28 | path: './node_modules/cz-test', 29 | }, 30 | }, 31 | }; 32 | 33 | const content = JSON.stringify(metadata); 34 | const result = commitizenSpecialParser(content, '/a/package.json', [], '/a'); 35 | result.should.deepEqual(['cz-test']); 36 | }); 37 | 38 | it('should recognize the module used by commitizen (short style)', () => { 39 | const metadata = { 40 | config: { 41 | commitizen: { 42 | path: 'cz-test', 43 | }, 44 | }, 45 | }; 46 | 47 | const content = JSON.stringify(metadata); 48 | const result = commitizenSpecialParser(content, '/a/package.json', [], '/a'); 49 | result.should.deepEqual(['cz-test']); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import lodash from 'lodash'; 3 | import component from './component.json'; 4 | 5 | function constructComponent(source, name) { 6 | return lodash(source[name]) 7 | .map(file => [ 8 | file, 9 | require(path.resolve(__dirname, name, file)), // eslint-disable-line global-require 10 | ]) 11 | .fromPairs() 12 | .value(); 13 | } 14 | 15 | export const availableParsers = constructComponent(component, 'parser'); 16 | 17 | export const availableDetectors = constructComponent(component, 'detector'); 18 | 19 | export const availableSpecials = constructComponent(component, 'special'); 20 | 21 | export const defaultOptions = { 22 | withoutDev: false, 23 | ignoreBinPackage: false, 24 | ignoreMatches: [ 25 | ], 26 | ignoreDirs: [ 27 | '.git', 28 | '.svn', 29 | '.hg', 30 | '.idea', 31 | 'node_modules', 32 | 'bower_components', 33 | ], 34 | skipMissing: false, 35 | parsers: { 36 | '*.js': availableParsers.jsx, 37 | '*.jsx': availableParsers.jsx, 38 | '*.coffee': availableParsers.coffee, 39 | '*.litcoffee': availableParsers.coffee, 40 | '*.coffee.md': availableParsers.coffee, 41 | '*.ts': availableParsers.typescript, 42 | '*.tsx': availableParsers.typescript, 43 | '*.sass': availableParsers.sass, 44 | '*.scss': availableParsers.sass, 45 | }, 46 | detectors: [ 47 | availableDetectors.importDeclaration, 48 | availableDetectors.requireCallExpression, 49 | availableDetectors.requireResolveCallExpression, 50 | availableDetectors.gruntLoadTaskCallExpression, 51 | ], 52 | specials: lodash.values(availableSpecials), 53 | }; 54 | -------------------------------------------------------------------------------- /src/special/bin.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import lodash from 'lodash'; 3 | import { readJSON, getScripts } from '../utils'; 4 | 5 | const metadataCache = {}; 6 | 7 | function getCacheOrRequire(packagePath) { 8 | if (metadataCache[packagePath]) { 9 | return metadataCache[packagePath]; 10 | } 11 | 12 | const metadata = readJSON(packagePath); 13 | metadataCache[packagePath] = metadata; 14 | return metadata; 15 | } 16 | 17 | function loadMetadata(dep, dir) { 18 | try { 19 | const packagePath = path.resolve(dir, 'node_modules', dep, 'package.json'); 20 | return getCacheOrRequire(packagePath); 21 | } catch (error) { 22 | return {}; // ignore silently 23 | } 24 | } 25 | 26 | function getBinaryFeatures(dep, [key, value]) { 27 | const binPath = path.join('node_modules', dep, value).replace(/\\/g, '/'); 28 | 29 | const features = [ 30 | key, 31 | `--require ${key}`, 32 | `--require ${key}/register`, 33 | `$(npm bin)/${key}`, 34 | `node_modules/.bin/${key}`, 35 | `./node_modules/.bin/${key}`, 36 | binPath, 37 | `./${binPath}`, 38 | ]; 39 | 40 | return features; 41 | } 42 | 43 | function isBinaryInUse(dep, scripts, dir) { 44 | const metadata = loadMetadata(dep, dir); 45 | const binaries = lodash.toPairs(metadata.bin || {}); 46 | return binaries.some(bin => 47 | getBinaryFeatures(dep, bin).some(feature => 48 | scripts.some(script => 49 | lodash.includes(` ${script} `, ` ${feature} `)))); 50 | } 51 | 52 | export default function parseBinary(content, filepath, deps, dir) { 53 | const scripts = getScripts(filepath, content); 54 | return deps.filter(dep => isBinaryInUse(dep, scripts, dir)); 55 | } 56 | -------------------------------------------------------------------------------- /test/special/mocha.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | import 'should'; 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import parse from '../../src/special/mocha'; 7 | 8 | describe('mocha special parser', () => { 9 | it('should ignore when filename is not supported', () => { 10 | const result = parse('content', 'not-supported.txt', [], __dirname); 11 | result.should.deepEqual([]); 12 | }); 13 | 14 | it('should recognize dependencies used in default mocha options', () => { 15 | const content = ['--require chai', '--ui bdd', '--reporter spec'].join('\n'); 16 | const optPath = path.resolve(__dirname, 'test/mocha.opts'); 17 | const result = parse(content, optPath, ['chai', 'unused'], __dirname); 18 | result.should.deepEqual(['chai']); 19 | }); 20 | 21 | it('should recognize dependencies path-module used in mocha options', () => { 22 | const content = ['--require chai/path/to/module', '--ui bdd', '--reporter spec'].join('\n'); 23 | const optPath = path.resolve(__dirname, 'test/mocha.opts'); 24 | const result = parse(content, optPath, ['chai', 'unused'], __dirname); 25 | result.should.deepEqual(['chai']); 26 | }); 27 | 28 | it('should recognize mocha options specified from scripts', () => { 29 | const rootDir = path.resolve(__dirname, '../fake_modules/mocha_opts'); 30 | const packagePath = path.resolve(rootDir, 'package.json'); 31 | const packageContent = fs.readFileSync(packagePath, 'utf-8'); 32 | const dependencies = Object.keys(JSON.parse(packageContent).devDependencies); 33 | const result = parse(packageContent, packagePath, dependencies, rootDir); 34 | result.should.deepEqual(['babel', 'chai']); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/special/gulp-load-plugins.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | import 'should'; 4 | import parse from '../../src/special/gulp-load-plugins'; 5 | 6 | describe('gulp-load-plugins special parser', () => { 7 | it('should ignore when file is not `gulpfile.js`', () => { 8 | const result = parse('content', '/path/to/not-gulpfile.js', [], '/path/to'); 9 | result.should.deepEqual([]); 10 | }); 11 | 12 | const testCases = { 13 | 'dependency with pattern `gulp-*`': { 14 | dependency: 'gulp-jshint', 15 | code: ` 16 | const gulpLoadPlugins = require('gulp-load-plugins'); 17 | const $ = gulpLoadPlugins(); 18 | $.jshint(); 19 | `, 20 | }, 21 | 'dependency with pattern `gulp.*`': { 22 | dependency: 'gulp.concat', 23 | code: ` 24 | const $ = require('gulp-load-plugins')(); 25 | $.concat(); 26 | `, 27 | }, 28 | 'dependency with name containing multiple dash signs': { 29 | dependency: 'gulp-this-plugin', 30 | code: ` 31 | require('gulp-load-plugins')().thisPlugin(); 32 | `, 33 | }, 34 | 'scoped dependency': { 35 | dependency: '@scope/gulp-plugin', 36 | code: ` 37 | import gulpLoadPlugins from 'gulp-load-plugins'; 38 | const $ = gulpLoadPlugins(); 39 | $.scope.plugin(); 40 | `, 41 | }, 42 | 'dependency used in direct call': { 43 | dependency: 'gulp-sourcemaps', 44 | code: ` 45 | const $ = require('gulp-load-plugins')(); 46 | $.sourcemaps.init(); 47 | `, 48 | }, 49 | }; 50 | 51 | ['gulpfile.js', 'gulpfile.babel.js'].forEach(gulpFileName => 52 | Object.keys(testCases).forEach(name => 53 | it(`should recognize ${name}`, () => { 54 | const testCase = testCases[name]; 55 | const result = parse( 56 | testCase.code, 57 | `/path/to/${gulpFileName}`, 58 | [testCase.dependency, 'gulp-load-plugins'], 59 | '/path/to', 60 | ); 61 | 62 | result.should.deepEqual([testCase.dependency]); 63 | }))); 64 | }); 65 | -------------------------------------------------------------------------------- /src/special/babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import lodash from 'lodash'; 3 | 4 | function parse(content) { 5 | try { 6 | return JSON.parse(content); 7 | } catch (error) { 8 | return {}; // ignore parse error silently 9 | } 10 | } 11 | 12 | function isPlugin(target, plugin) { 13 | return lodash.isString(target) 14 | ? target === plugin || target === `babel-plugin-${plugin}` 15 | : target[0] === plugin || target[0] === `babel-plugin-${plugin}`; 16 | } 17 | 18 | function contain(array, dep, prefix) { 19 | if (!array) { 20 | return false; 21 | } 22 | 23 | // extract name if wrapping with options 24 | const names = array.map(item => (lodash.isString(item) ? item : item[0])); 25 | if (names.indexOf(dep) !== -1) { 26 | return true; 27 | } 28 | 29 | if (prefix && dep.indexOf(prefix) === 0) { 30 | return contain(array, dep.substring(prefix.length), false); 31 | } 32 | 33 | return false; 34 | } 35 | 36 | function getReactTransforms(deps, plugins) { 37 | const transforms = lodash(plugins || []) 38 | .filter(plugin => isPlugin(plugin, 'react-transform')) 39 | .map(([, plugin]) => plugin.transforms.map(({ transform }) => transform)) 40 | .first(); 41 | 42 | return lodash.intersection(transforms, deps); 43 | } 44 | 45 | function filter(deps, options) { 46 | const presets = deps.filter(dep => 47 | contain(options.presets, dep, 'babel-preset-')); 48 | 49 | const plugins = deps.filter(dep => 50 | contain(options.plugins, dep, 'babel-plugin-')); 51 | 52 | const reactTransforms = getReactTransforms(deps, options.plugins); 53 | 54 | return presets.concat(plugins, reactTransforms); 55 | } 56 | 57 | function checkOptions(deps, options = {}) { 58 | const optDeps = filter(deps, options); 59 | const envDeps = lodash(options.env) 60 | .values() 61 | .map(env => filter(deps, env)) 62 | .flatten() 63 | .value(); 64 | 65 | return optDeps.concat(envDeps); 66 | } 67 | 68 | export default function parseBabel(content, filePath, deps) { 69 | const filename = path.basename(filePath); 70 | 71 | if (filename === '.babelrc') { 72 | const options = parse(content); 73 | return checkOptions(deps, options); 74 | } 75 | 76 | if (filename === 'package.json') { 77 | const metadata = parse(content); 78 | return checkOptions(deps, metadata.babel); 79 | } 80 | 81 | return []; 82 | } 83 | -------------------------------------------------------------------------------- /src/special/webpack.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import lodash from 'lodash'; 3 | 4 | const webpackConfigRegex = /webpack(\..+)?\.config\.(babel\.)?js/; 5 | const loaderTemplates = ['*-webpack-loader', '*-web-loader', '*-loader', '*']; 6 | 7 | function extractLoaders(item) { 8 | if (typeof item === 'string') { 9 | return item; 10 | } 11 | 12 | if (item.loader && typeof item.loader === 'string') { 13 | return item.loader.split('!'); 14 | } 15 | 16 | if (item.loaders) { 17 | return item.loaders; 18 | } 19 | 20 | return []; 21 | } 22 | 23 | function stripQueryParameter(loader) { 24 | const index = loader.indexOf('?'); 25 | return index === -1 ? loader : loader.substring(0, index); 26 | } 27 | 28 | function normalizeLoader(deps, loader) { 29 | const name = lodash(loaderTemplates) 30 | .map(template => template.replace('*', loader)) 31 | .intersection(deps) 32 | .first(); 33 | return name; 34 | } 35 | 36 | function getLoaders(deps, loaders) { 37 | return lodash(loaders || []) 38 | .map(extractLoaders) 39 | .flatten() 40 | .map(loader => stripQueryParameter(loader)) 41 | .map(loader => normalizeLoader(deps, loader)) 42 | .filter(loader => loader) 43 | .uniq() 44 | .value(); 45 | } 46 | 47 | function parseWebpack1(module, deps) { 48 | const loaders = getLoaders(deps, module.loaders); 49 | const preLoaders = getLoaders(deps, module.preLoaders); 50 | const postLoaders = getLoaders(deps, module.postLoaders); 51 | return [...loaders, ...preLoaders, ...postLoaders]; 52 | } 53 | 54 | function mapRuleUse(module) { 55 | return module.rules 56 | // filter use or loader because 'loader' is a shortcut to 'use' 57 | .filter(rule => rule.use || rule.loader) 58 | // return coerced array, using the relevant key 59 | .map(rule => [].concat(rule.use || rule.loader)); 60 | } 61 | 62 | function parseWebpack2(module, deps) { 63 | if (!module.rules) { 64 | return []; 65 | } 66 | 67 | const mappedLoaders = module.rules.filter(rule => rule.loaders); 68 | const mappedUses = mapRuleUse(module); 69 | const loaders = getLoaders(deps, lodash.flatten([...mappedLoaders, ...mappedUses])); 70 | return loaders; 71 | } 72 | 73 | export default function parseWebpack(content, filepath, deps) { 74 | const filename = path.basename(filepath); 75 | if (webpackConfigRegex.test(filename)) { 76 | const module = require(filepath).module || {}; // eslint-disable-line global-require 77 | 78 | const webpack1Loaders = parseWebpack1(module, deps); 79 | const webpack2Loaders = parseWebpack2(module, deps); 80 | return [...webpack1Loaders, ...webpack2Loaders]; 81 | } 82 | 83 | return []; 84 | } 85 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import lodash from 'lodash'; 3 | import minimatch from 'minimatch'; 4 | import check from './check'; 5 | import { readJSON } from './utils'; 6 | 7 | import { 8 | defaultOptions, 9 | availableParsers, 10 | availableDetectors, 11 | availableSpecials, 12 | } from './constants'; 13 | 14 | function isIgnored(ignoreMatches, dependency) { 15 | const match = lodash.partial(minimatch, dependency); 16 | return ignoreMatches.some(match); 17 | } 18 | 19 | function hasBin(rootDir, dependency) { 20 | try { 21 | const metadata = readJSON(path.join(rootDir, 'node_modules', dependency, 'package.json')); 22 | return {}.hasOwnProperty.call(metadata, 'bin'); 23 | } catch (error) { 24 | return false; 25 | } 26 | } 27 | 28 | function filterDependencies(rootDir, ignoreBinPackage, ignoreMatches, dependencies) { 29 | return lodash(dependencies) 30 | .keys() 31 | .reject(dep => 32 | (isIgnored(ignoreMatches, dep)) || 33 | (ignoreBinPackage && hasBin(rootDir, dep))) 34 | .value(); 35 | } 36 | 37 | export default function depcheck(rootDir, options, callback) { 38 | const getOption = key => 39 | (lodash.isUndefined(options[key]) ? defaultOptions[key] : options[key]); 40 | 41 | const withoutDev = getOption('withoutDev'); 42 | const ignoreBinPackage = getOption('ignoreBinPackage'); 43 | const ignoreMatches = getOption('ignoreMatches'); 44 | const ignoreDirs = lodash.union(defaultOptions.ignoreDirs, options.ignoreDirs); 45 | const skipMissing = getOption('skipMissing'); 46 | 47 | const detectors = getOption('detectors'); 48 | const parsers = lodash(getOption('parsers')) 49 | .mapValues(value => (lodash.isArray(value) ? value : [value])) 50 | .merge({ '*': getOption('specials') }) 51 | .value(); 52 | 53 | const metadata = options.package || readJSON(path.join(rootDir, 'package.json')); 54 | const dependencies = metadata.dependencies || {}; 55 | const devDependencies = !withoutDev && metadata.devDependencies ? metadata.devDependencies : {}; 56 | const peerDeps = Object.keys(metadata.peerDependencies || {}); 57 | const optionalDeps = Object.keys(metadata.optionalDependencies || {}); 58 | const deps = filterDependencies(rootDir, ignoreBinPackage, ignoreMatches, dependencies); 59 | const devDeps = filterDependencies(rootDir, ignoreBinPackage, ignoreMatches, devDependencies); 60 | 61 | return check({ 62 | rootDir, 63 | ignoreDirs, 64 | skipMissing, 65 | deps, 66 | devDeps, 67 | peerDeps, 68 | optionalDeps, 69 | parsers, 70 | detectors, 71 | }) 72 | .then(results => Object.assign(results, { 73 | missing: lodash.pick( 74 | results.missing, 75 | filterDependencies(rootDir, ignoreBinPackage, ignoreMatches, results.missing), 76 | ), 77 | })) 78 | .then(callback); 79 | } 80 | 81 | depcheck.parser = availableParsers; 82 | depcheck.detector = availableDetectors; 83 | depcheck.special = availableSpecials; 84 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love contribution from everyone. Please open an issue if you encounter a bug, or have feature requests, or get a documentation issue, or anything. 4 | 5 | Issues tagged with [help-wanted](https://github.com/depcheck/depcheck/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22help+wanted%22+) are desired to get support. 6 | 7 | ## Setup machine 8 | 9 | Clone the repository, install the npm dependencies. That is all: 10 | 11 | ```sh 12 | git clone https://github.com/depcheck/depcheck.git 13 | cd depcheck 14 | npm install 15 | ``` 16 | 17 | ## Design 18 | 19 | Please read and understand the [pluggable design](https://github.com/depcheck/depcheck/blob/master/doc/pluggable-design.md) if you are going to implement new features. 20 | 21 | Basically, it divides the depcheck process into three steps handled by different components. 22 | 23 | 1. Walk the files under the depcheck directory, managed by depcheck itself. 24 | 2. Parse file to abstract syntax tree (aka, AST), handled by [parser](https://github.com/depcheck/depcheck/blob/master/doc/pluggable-design.md#parser). Besides, dependencies can be reported directly with [special parser](https://github.com/depcheck/depcheck/blob/master/doc/pluggable-design.md#special-parser). 25 | 3. Detect dependencies from AST, handled by [detector](https://github.com/depcheck/depcheck/blob/master/doc/pluggable-design.md#detector). 26 | 27 | ## Test driven 28 | 29 | Run test: 30 | 31 | ```sh 32 | npm run test 33 | ``` 34 | 35 | All modification should have test to support it and avoid regression if possible. 36 | 37 | Depcheck API and CLI test cases are defined in [spec.json](https://github.com/depcheck/depcheck/blob/master/test/spec.json). Special parsers test cases are defined in [their own test files](https://github.com/depcheck/depcheck/tree/master/test/special). 38 | 39 | ## Code style 40 | 41 | Run code style check: 42 | 43 | ```sh 44 | npm run lint 45 | ``` 46 | 47 | This project follows the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript), and is enforced by [eslint-config-airbnb](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb) preset. 48 | 49 | It is a must to pass code style check when you do code modifications. 50 | 51 | ## Self check 52 | 53 | Run self check commands: 54 | 55 | ```sh 56 | npm run depcheck 57 | npm run depcheck-json 58 | ``` 59 | 60 | To be a better tool, we decide to eat our own dog food. Self check will check the unused dependencies in this project. 61 | 62 | It might get false alerts or regressions if introducing new features. Some could be skipped using `--ignores` parameter as a workaround, some should be fixed to avoid broking existing features. Please let us know if you get into such situation. 63 | 64 | ## Pull request 65 | 66 | Please rebase to the top main branch when sending a pull request. Both Travis and AppVeyor check should be passed. 67 | 68 | Test coverage is minor. However, if there is a huge effect on test coverage, we could take a look. 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "depcheck", 3 | "version": "0.0.1", 4 | "description": "Check dependencies in your node module", 5 | "main": "dist/index.js", 6 | "engines": { 7 | "node": ">=4" 8 | }, 9 | "bin": { 10 | "depcheck": "bin/depcheck" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/depcheck/depcheck.git" 15 | }, 16 | "scripts": { 17 | "component": "babel-node ./build/component.js > ./dist/component.json", 18 | "compile": "babel src/ -d dist/", 19 | "depcheck": "node ./bin/depcheck", 20 | "depcheck-web": "node ./bin/depcheck --json | depcheck-web", 21 | "depcheck-json": "node ./bin/depcheck --json | babel-node ./build/check-json", 22 | "prepublishOnly": "npm run compile && npm run component", 23 | "lint": "eslint ./src ./test ./build", 24 | "test": "babel-node node_modules/mocha/bin/_mocha ./test ./test/special --timeout 10000", 25 | "test-dependent": "dependent-build", 26 | "test-coverage": "babel-node node_modules/cross-env/dist/bin/cross-env.js NODE_ENV=test nyc mocha ./test ./test/special --timeout 20000 && nyc report --reporter=text-lcov > ./coverage/coverage.lcov" 27 | }, 28 | "author": { 29 | "name": "Djordje Lukic", 30 | "email": "lukic.djordje@gmail.com" 31 | }, 32 | "contributors": [ 33 | { 34 | "name": "Junle Li", 35 | "email": "lijunle@gmail.com" 36 | } 37 | ], 38 | "keywords": [ 39 | "check", 40 | "unused", 41 | "package", 42 | "packages", 43 | "depcheck", 44 | "dependency", 45 | "dependencies", 46 | "devDependencies" 47 | ], 48 | "license": "MIT", 49 | "readmeFilename": "README.md", 50 | "dependencies": { 51 | "babel-traverse": "^6.7.3", 52 | "babylon": "^6.1.21", 53 | "builtin-modules": "^1.1.1", 54 | "deprecate": "^1.0.0", 55 | "deps-regex": "^0.1.4", 56 | "js-yaml": "^3.4.2", 57 | "lodash": "^4.5.1", 58 | "minimatch": "^3.0.2", 59 | "require-package-name": "^2.0.1", 60 | "walkdir": "0.0.11", 61 | "yargs": "^8.0.2" 62 | }, 63 | "devDependencies": { 64 | "babel-cli": "^6.1.1", 65 | "babel-eslint": "^7.0.0", 66 | "babel-plugin-add-module-exports": "^0.2.1", 67 | "babel-plugin-istanbul": "^4.1.4", 68 | "babel-plugin-transform-object-assign": "^6.1.18", 69 | "babel-polyfill": "^6.16.0", 70 | "babel-preset-es2015": "^6.0.15", 71 | "babel-preset-stage-2": "^6.0.15", 72 | "babel-register": "^6.18.0", 73 | "codecov.io": "^0.1.6", 74 | "cross-env": "^5.0.1", 75 | "depcheck-web": "^0.1.0", 76 | "dependent-build": "^0.1.2", 77 | "eslint": "^3.10.1", 78 | "eslint-config-airbnb": "^15.0.2", 79 | "eslint-plugin-import": "^2.6.1", 80 | "eslint-plugin-jsx-a11y": "^5.1.1", 81 | "eslint-plugin-react": "^7.1.0", 82 | "fs-extra": "^4.0.0", 83 | "mocha": "^3.0.0", 84 | "node-sass": "^4.5.3", 85 | "nyc": "^11.0.3", 86 | "patch-version": "^0.1.1", 87 | "should": "^11.0.0", 88 | "typescript": "^2.4.1" 89 | }, 90 | "nyc": { 91 | "sourceMap": false, 92 | "instrument": false, 93 | "exclude": [ 94 | "dist", 95 | "test" 96 | ], 97 | "require": [ 98 | "babel-polyfill", 99 | "babel-register" 100 | ], 101 | "reporter": [ 102 | "html", 103 | "text" 104 | ] 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /test/special/babel.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | import 'should'; 4 | import parse from '../../src/special/babel'; 5 | 6 | const testCases = [ 7 | { 8 | name: 'handle no options case', 9 | deps: [], 10 | options: undefined, 11 | }, 12 | { 13 | name: 'recognize the short-name plugin', 14 | deps: ['babel-plugin-syntax-jsx'], 15 | options: { 16 | plugins: ['syntax-jsx'], 17 | }, 18 | }, 19 | { 20 | name: 'recognize the long-name plugin', 21 | deps: ['babel-plugin-syntax-jsx'], 22 | options: { 23 | plugins: ['babel-plugin-syntax-jsx'], 24 | }, 25 | }, 26 | { 27 | name: 'recognize the short-name preset', 28 | deps: ['babel-preset-es2015'], 29 | options: { 30 | presets: ['es2015'], 31 | }, 32 | }, 33 | { 34 | name: 'recognize the long-name preset', 35 | deps: ['babel-preset-es2015'], 36 | options: { 37 | presets: ['babel-preset-es2015'], 38 | }, 39 | }, 40 | { 41 | name: 'recognize plugin specified with options', 42 | deps: ['babel-plugin-transform-async-to-module-method'], 43 | options: { 44 | plugins: [ 45 | ['transform-async-to-module-method', { 46 | module: 'bluebird', 47 | method: 'coroutine', 48 | }], 49 | ], 50 | }, 51 | }, 52 | { 53 | name: 'recognize tranforms used in babel-plugin-react-transform', 54 | deps: ['babel-plugin-react-transform', 'react-transform-hmr', 'react-transform-catch-errors'], 55 | options: { 56 | plugins: [ 57 | [ 58 | 'react-transform', 59 | { 60 | transforms: [ 61 | { 62 | transform: 'react-transform-hmr', 63 | imports: ['react'], 64 | locals: ['module'], 65 | }, 66 | { 67 | transform: 'react-transform-catch-errors', 68 | imports: ['react', 'redbox-react'], 69 | }, 70 | { 71 | transform: './my-custom-transform', 72 | }, 73 | ], 74 | }, 75 | ], 76 | ], 77 | }, 78 | }, 79 | ]; 80 | 81 | function testBabel(filename, deps, content) { 82 | const result = parse(content ? JSON.stringify(content) : '', filename, deps); 83 | result.should.deepEqual(deps); 84 | } 85 | 86 | describe('babel special parser', () => { 87 | it('should ignore when filename is not supported', () => { 88 | const result = parse('content', 'not-supported.txt', ['deps']); 89 | result.should.deepEqual([]); 90 | }); 91 | 92 | it('should recognize dependencies not a babel plugin', () => { 93 | const content = JSON.stringify({ 94 | presets: ['es2015'], 95 | }); 96 | 97 | const result = parse(content, '/path/to/.babelrc', ['babel-preset-es2015', 'dep']); 98 | result.should.deepEqual(['babel-preset-es2015']); 99 | }); 100 | 101 | testCases.forEach(testCase => 102 | it(`should ${testCase.name} in .babelrc file`, () => 103 | testBabel('.babelrc', testCase.deps, testCase.options))); 104 | 105 | testCases.forEach(testCase => 106 | it(`should ${testCase.name} inside .babelrc file env section`, () => 107 | testBabel('.babelrc', testCase.deps, { 108 | env: { 109 | production: testCase.options, 110 | }, 111 | }))); 112 | 113 | testCases.forEach(testCase => 114 | it(`should ${testCase.name} in package.json file`, () => 115 | testBabel('package.json', testCase.deps, { 116 | name: 'my-package', 117 | version: '1.0.0', 118 | babel: testCase.options, 119 | }))); 120 | 121 | testCases.forEach(testCase => 122 | it(`should ${testCase.name} inside package.json file env section`, () => 123 | testBabel('package.json', testCase.deps, { 124 | name: 'my-package', 125 | version: '1.0.0', 126 | babel: { 127 | env: { 128 | development: testCase.options, 129 | }, 130 | }, 131 | }))); 132 | }); 133 | -------------------------------------------------------------------------------- /test/special/bin.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | import 'should'; 4 | import parse from '../../src/special/bin'; 5 | 6 | const testCases = [ 7 | { 8 | name: 'detect packages used in scripts', 9 | script: 'binary-entry --argument', 10 | dependencies: ['binary-package'], 11 | expected: ['binary-package'], 12 | }, 13 | { 14 | name: 'detect packages used as `.bin` path', 15 | script: './node_modules/.bin/binary-entry', 16 | dependencies: ['binary-package'], 17 | expected: ['binary-package'], 18 | }, 19 | { 20 | name: 'detect packages used as package path', 21 | script: './node_modules/binary-package/bin/binary-exe', 22 | dependencies: ['binary-package'], 23 | expected: ['binary-package'], 24 | }, 25 | { 26 | name: 'detect packages combined with `npm bin` command', 27 | script: '$(npm bin)/binary-entry', 28 | dependencies: ['binary-package'], 29 | expected: ['binary-package'], 30 | }, 31 | { 32 | name: 'detect package bin without prefix dot', 33 | script: 'node_modules/.bin/binary-entry', 34 | dependencies: ['binary-package'], 35 | expected: ['binary-package'], 36 | }, 37 | { 38 | name: 'detect package path without prefix dot', 39 | script: 'node_modules/binary-package/bin/binary-exe', 40 | dependencies: ['binary-package'], 41 | expected: ['binary-package'], 42 | }, 43 | { 44 | name: 'detect binary call with variable set', 45 | script: 'NODE_ENV=production binary-entry', 46 | dependencies: ['binary-package'], 47 | expected: ['binary-package'], 48 | }, 49 | { 50 | name: 'not report it when it is not used', 51 | script: 'other-binary-entry', 52 | dependencies: ['binary-package'], 53 | expected: [], 54 | }, 55 | { 56 | name: 'ignore detection when no scripts section', 57 | script: false, 58 | dependencies: ['binary-package'], 59 | expected: [], 60 | }, 61 | { 62 | name: 'ignore dependency without bin entry', 63 | script: 'binary-entry', 64 | dependencies: ['binary-no-bin'], 65 | expected: [], 66 | }, 67 | { 68 | name: 'handle dependency without package.json', 69 | script: 'binary-entry', 70 | dependencies: ['binary-no-package'], 71 | expected: [], 72 | }, 73 | { 74 | name: 'detect packages used with --require and /register', 75 | script: 'module --require binary-entry/register', 76 | dependencies: ['binary-package'], 77 | expected: ['binary-package'], 78 | }, 79 | { 80 | name: 'detect packages used with --require', 81 | script: 'module --require binary-entry', 82 | dependencies: ['binary-package'], 83 | expected: ['binary-package'], 84 | }, 85 | ]; 86 | 87 | function testParser(testCase, content, filename) { 88 | const result = parse(content, filename, testCase.dependencies, __dirname); 89 | result.should.deepEqual(testCase.expected); 90 | } 91 | 92 | describe('bin special parser', () => { 93 | it('should ignore when filename is not supported', () => { 94 | const result = parse('content', 'not-supported.txt', [], '/root/dir'); 95 | result.should.deepEqual([]); 96 | }); 97 | 98 | describe('on `package.json`', () => 99 | testCases.forEach(testCase => 100 | it(`should ${testCase.name}`, () => { 101 | const content = testCase.script 102 | ? JSON.stringify({ scripts: { t: testCase.script } }) 103 | : '{}'; 104 | 105 | testParser(testCase, content, `/path/to/${testCase.name}/package.json`); 106 | }))); 107 | 108 | describe('on `.travis.yml`', () => 109 | testCases.forEach(testCase => 110 | it(`should ${testCase.name}`, () => { 111 | const content = testCase.script 112 | ? `script:\n - ${testCase.script}` 113 | : ''; 114 | 115 | testParser(testCase, content, `/path/to/${testCase.name}/.travis.yml`); 116 | }))); 117 | 118 | it('should check lifecycle commands in `.travis.yml` file', () => { 119 | const content = `before_deploy:\n - ${testCases[0].script}`; 120 | testParser(testCases[0], content, '/path/to/.travis.yml'); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/special/eslint.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import yaml from 'js-yaml'; 3 | import lodash from 'lodash'; 4 | import requirePackageName from 'require-package-name'; 5 | import { evaluate } from '../utils'; 6 | 7 | function parse(content) { 8 | try { 9 | return JSON.parse(content); 10 | } catch (error) { 11 | // not JSON format 12 | } 13 | 14 | try { 15 | return yaml.safeLoad(content); 16 | } catch (error) { 17 | // not YAML format 18 | } 19 | 20 | try { 21 | return evaluate(`module.exports = ${content}`); 22 | } catch (error) { 23 | // not valid JavaScript code 24 | } 25 | 26 | // parse fail, return nothing 27 | return {}; 28 | } 29 | 30 | function wrapToArray(obj) { 31 | if (!obj) { 32 | return []; 33 | } else if (lodash.isArray(obj)) { 34 | return obj; 35 | } 36 | 37 | return [obj]; 38 | } 39 | 40 | function isEslintConfigAnAbsolutePath(specifier) { 41 | return path.isAbsolute(specifier); 42 | } 43 | 44 | function isEslintConfigARelativePath(specifier) { 45 | return lodash.startsWith(specifier, './') || lodash.startsWith(specifier, '../'); 46 | } 47 | 48 | function isEslintConfigFromAPlugin(specifier) { 49 | return lodash.startsWith(specifier, 'plugin:'); 50 | } 51 | 52 | function isEslintConfigFromAScopedModule(specifier) { 53 | return lodash.startsWith(specifier, '@'); 54 | } 55 | 56 | function isEslintConfigFromAFullyQualifiedModuleName(specifier, prefix) { 57 | return lodash.startsWith(specifier, prefix); 58 | } 59 | 60 | function resolvePresetPackage(preset, rootDir) { 61 | // inspired from https://github.com/eslint/eslint/blob/5b4a94e26d0ef247fe222dacab5749805d9780dd/lib/config/config-file.js#L347 62 | if (isEslintConfigAnAbsolutePath(preset)) { 63 | return preset; 64 | } 65 | if (isEslintConfigARelativePath(preset)) { 66 | return path.resolve(rootDir, preset); 67 | } 68 | 69 | const { prefix, specifier } = ( 70 | isEslintConfigFromAPlugin(preset) 71 | ? { prefix: 'eslint-plugin-', specifier: preset.substring(preset.indexOf(':') + 1) } 72 | : { prefix: 'eslint-config-', specifier: preset } 73 | ); 74 | 75 | if (isEslintConfigFromAScopedModule(specifier)) { 76 | const scope = specifier.substring(0, specifier.indexOf('/')); 77 | const module = specifier.substring(specifier.indexOf('/') + 1); 78 | 79 | if (isEslintConfigFromAFullyQualifiedModuleName(module, prefix)) { 80 | return specifier; 81 | } 82 | return `${scope}/${prefix}${module}`; 83 | } 84 | if (isEslintConfigFromAFullyQualifiedModuleName(specifier, prefix)) { 85 | return specifier; 86 | } 87 | return `${prefix}${specifier}`; 88 | } 89 | 90 | function loadConfig(preset, rootDir) { 91 | const presetPath = path.isAbsolute(preset) 92 | ? preset 93 | : path.resolve(rootDir, 'node_modules', preset); 94 | 95 | try { 96 | return require(presetPath); // eslint-disable-line global-require 97 | } catch (error) { 98 | return {}; // silently return nothing 99 | } 100 | } 101 | 102 | function checkConfig(config, rootDir) { 103 | const parser = wrapToArray(config.parser); 104 | const plugins = wrapToArray(config.plugins).map(plugin => `eslint-plugin-${plugin}`); 105 | 106 | const presets = wrapToArray(config.extends) 107 | .filter(preset => preset !== 'eslint:recommended') 108 | .map(preset => resolvePresetPackage(preset, rootDir)); 109 | 110 | const presetPackages = presets 111 | .filter(preset => !path.isAbsolute(preset)) 112 | .map(requirePackageName); 113 | 114 | const presetDeps = lodash(presets) 115 | .map(preset => loadConfig(preset, rootDir)) 116 | .map(presetConfig => checkConfig(presetConfig, rootDir)) 117 | .flatten() 118 | .value(); 119 | 120 | return lodash.union(parser, plugins, presetPackages, presetDeps); 121 | } 122 | 123 | export default function parseESLint(content, filename, deps, rootDir) { 124 | const basename = path.basename(filename); 125 | if (/^\.eslintrc(\.json|\.js|\.yml|\.yaml)?$/.test(basename)) { 126 | const config = parse(content); 127 | return checkConfig(config, rootDir); 128 | } 129 | 130 | return []; 131 | } 132 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import yargs from 'yargs'; 4 | import lodash from 'lodash'; 5 | import deprecate from 'deprecate'; 6 | 7 | import depcheck from './index'; 8 | import { version } from '../package.json'; 9 | 10 | function checkPathExist(dir, errorMessage) { 11 | return new Promise((resolve, reject) => 12 | fs.exists(dir, result => 13 | (result ? resolve() : reject(errorMessage)))); 14 | } 15 | 16 | function getParsers(parsers) { 17 | return lodash.isUndefined(parsers) 18 | ? undefined 19 | : lodash(parsers) 20 | .split(',') 21 | .map(keyValuePair => keyValuePair.split(':')) 22 | .fromPairs() 23 | .mapValues(value => value.split('&').map(name => depcheck.parser[name])) 24 | .value(); 25 | } 26 | 27 | function getDetectors(detectors) { 28 | return lodash.isUndefined(detectors) 29 | ? undefined 30 | : detectors.split(',').map(name => depcheck.detector[name]); 31 | } 32 | 33 | function getSpecials(specials) { 34 | return lodash.isUndefined(specials) 35 | ? undefined 36 | : specials.split(',').map(name => depcheck.special[name]); 37 | } 38 | 39 | function noIssue(result) { 40 | return lodash.isEmpty(result.dependencies) 41 | && lodash.isEmpty(result.devDependencies) 42 | && lodash.isEmpty(result.missing); 43 | } 44 | 45 | function prettify(caption, deps) { 46 | const list = deps.map(dep => `* ${dep}`); 47 | return list.length ? [caption].concat(list) : []; 48 | } 49 | 50 | function print(result, log, json) { 51 | if (json) { 52 | log(JSON.stringify(result, (key, value) => (lodash.isError(value) ? value.stack : value))); 53 | } else if (noIssue(result)) { 54 | log('No depcheck issue'); 55 | } else { 56 | const deps = prettify('Unused dependencies', result.dependencies); 57 | const devDeps = prettify('Unused devDependencies', result.devDependencies); 58 | const missing = prettify('Missing dependencies', Object.keys(result.missing)); 59 | const content = deps.concat(devDeps, missing).join('\n'); 60 | log(content); 61 | } 62 | 63 | return result; 64 | } 65 | 66 | function checkDeprecation(argv) { 67 | if (argv.dev === false) { 68 | deprecate( 69 | 'The option `dev` is deprecated. It leads a wrong result for missing dependencies' + 70 | ' when it is `false`. This option will be removed and enforced to `true` in next' + 71 | ' major version.', 72 | ); 73 | } 74 | } 75 | 76 | export default function cli(args, log, error, exit) { 77 | const opt = yargs(args) 78 | .usage('Usage: $0 [DIRECTORY]') 79 | .boolean([ 80 | 'dev', 81 | 'ignore-bin-package', 82 | 'skip-missing', 83 | ]) 84 | .default({ 85 | dev: true, 86 | 'ignore-bin-package': false, 87 | 'skip-missing': false, 88 | }) 89 | .describe('dev', '[DEPRECATED] Check on devDependecies') 90 | .describe('ignore-bin-package', 'Ignore package with bin entry') 91 | .describe('skip-missing', 'Skip calculation of missing dependencies') 92 | .describe('json', 'Output results to JSON') 93 | .describe('ignores', 'Comma separated package list to ignore') 94 | .describe('ignore-dirs', 'Comma separated folder names to ignore') 95 | .describe('parsers', 'Comma separated glob:pasers pair list') 96 | .describe('detectors', 'Comma separated detector list') 97 | .describe('specials', 'Comma separated special parser list') 98 | .version('version', 'Show version number', version) 99 | .help('help', 'Show this help message'); 100 | 101 | checkDeprecation(opt.argv); 102 | 103 | const dir = opt.argv._[0] || '.'; 104 | const rootDir = path.resolve(dir); 105 | 106 | checkPathExist(rootDir, `Path ${dir} does not exist`) 107 | .then(() => checkPathExist( 108 | path.resolve(rootDir, 'package.json'), 109 | `Path ${dir} does not contain a package.json file`)) 110 | .then(() => depcheck(rootDir, { 111 | withoutDev: !opt.argv.dev, 112 | ignoreBinPackage: opt.argv.ignoreBinPackage, 113 | ignoreMatches: (opt.argv.ignores || '').split(','), 114 | ignoreDirs: (opt.argv.ignoreDirs || '').split(','), 115 | parsers: getParsers(opt.argv.parsers), 116 | detectors: getDetectors(opt.argv.detectors), 117 | specials: getSpecials(opt.argv.specials), 118 | skipMissing: opt.argv.skipMissing, 119 | })) 120 | .then(result => print(result, log, opt.argv.json)) 121 | .then(result => exit((opt.argv.json || noIssue(result)) ? 0 : -1)) 122 | .catch((errorMessage) => { 123 | error(errorMessage); 124 | exit(-1); 125 | }); 126 | } 127 | -------------------------------------------------------------------------------- /test/special/eslint.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | import 'should'; 4 | import yaml from 'js-yaml'; 5 | import eslintSpecialParser from '../../src/special/eslint'; 6 | 7 | const testCases = [ 8 | { 9 | name: 'ignore when user not extends any config in `.eslintrc`', 10 | content: {}, 11 | expected: [], 12 | }, 13 | { 14 | name: 'detect specific parser', 15 | content: { 16 | parser: 'babel-eslint', 17 | }, 18 | expected: [ 19 | 'babel-eslint', 20 | ], 21 | }, 22 | { 23 | name: 'detect specific plugins', 24 | content: { 25 | plugins: ['mocha'], 26 | }, 27 | expected: [ 28 | 'eslint-plugin-mocha', 29 | ], 30 | }, 31 | { 32 | name: 'handle eslint config with short name', 33 | content: { 34 | extends: 'preset', 35 | }, 36 | expected: [ 37 | 'eslint-config-preset', 38 | ], 39 | }, 40 | { 41 | name: 'handle eslint config with full name', 42 | content: { 43 | extends: 'eslint-config-preset', 44 | }, 45 | expected: [ 46 | 'eslint-config-preset', 47 | ], 48 | }, 49 | { 50 | name: 'handle eslint config from package module', 51 | content: { 52 | extends: 'airbnb/base', 53 | }, 54 | expected: [ 55 | 'eslint-config-airbnb', 56 | ], 57 | }, 58 | { 59 | name: 'handle eslint config with undeclared plugins', 60 | content: { 61 | extends: 'airbnb/react', 62 | }, 63 | expected: [ 64 | 'eslint-config-airbnb', 65 | 'eslint-plugin-react', 66 | ], 67 | }, 68 | { 69 | name: 'handle eslint config with nested extends', 70 | content: { 71 | extends: 'airbnb', 72 | }, 73 | expected: [ 74 | 'eslint-config-airbnb', 75 | 'eslint-plugin-react', 76 | ], 77 | }, 78 | { 79 | name: 'skip eslint recommended config', 80 | content: { 81 | extends: 'eslint:recommended', 82 | }, 83 | expected: [], 84 | }, 85 | { 86 | name: 'handle config of absolute local path', 87 | content: { 88 | extends: '/path/to/config', 89 | }, 90 | expected: [], 91 | }, 92 | { 93 | name: 'handle config of relative local path', 94 | content: { 95 | extends: './config', 96 | }, 97 | expected: [], 98 | }, 99 | { 100 | name: 'handle config of scoped module', 101 | content: { 102 | extends: '@my-org/short-customized', 103 | }, 104 | expected: [ 105 | '@my-org/eslint-config-short-customized', 106 | ], 107 | }, 108 | { 109 | name: 'handle config of scoped module with full name', 110 | content: { 111 | extends: '@my-org/eslint-config-long-customized', 112 | }, 113 | expected: [ 114 | '@my-org/eslint-config-long-customized', 115 | ], 116 | }, 117 | { 118 | name: 'handle config from plugin with short name', 119 | content: { 120 | extends: 'plugin:node/recommended', 121 | }, 122 | expected: [ 123 | 'eslint-plugin-node', 124 | ], 125 | }, 126 | { 127 | name: 'handle config from plugin with full name', 128 | content: { 129 | extends: 'plugin:eslint-plugin-node/recommended', 130 | }, 131 | expected: [ 132 | 'eslint-plugin-node', 133 | ], 134 | }, 135 | { 136 | name: 'handle config from scoped plugin with short name', 137 | content: { 138 | extends: 'plugin:@my-org/short-customized/recommended', 139 | }, 140 | expected: [ 141 | '@my-org/eslint-plugin-short-customized', 142 | ], 143 | }, 144 | { 145 | name: 'handle config from scoped plugin with full name', 146 | content: { 147 | extends: 'plugin:@my-org/eslint-plugin-long-customized/recommended', 148 | }, 149 | expected: [ 150 | '@my-org/eslint-plugin-long-customized', 151 | ], 152 | }, 153 | ]; 154 | 155 | function testEslint(deps, content) { 156 | [ 157 | '/path/to/.eslintrc', 158 | '/path/to/.eslintrc.js', 159 | '/path/to/.eslintrc.json', 160 | '/path/to/.eslintrc.yml', 161 | '/path/to/.eslintrc.yaml', 162 | ].forEach((pathToEslintrc) => { 163 | const result = eslintSpecialParser( 164 | content, pathToEslintrc, deps, __dirname); 165 | 166 | result.should.deepEqual(deps); 167 | }); 168 | } 169 | 170 | describe('eslint special parser', () => { 171 | it('should ignore when filename is not `.eslintrc`', () => { 172 | const result = eslintSpecialParser('content', '/a/file'); 173 | result.should.deepEqual([]); 174 | }); 175 | 176 | it('should handle parse error', () => 177 | testEslint([], '{ this is an invalid JSON string')); 178 | 179 | it('should handle non-standard JSON content', () => 180 | testEslint( 181 | testCases[1].expected, 182 | `${JSON.stringify(testCases[1].content)}\n// this is ignored`)); 183 | 184 | describe('with JSON format', () => 185 | testCases.forEach(testCase => 186 | it(`should ${testCase.name}`, () => 187 | testEslint(testCase.expected, JSON.stringify(testCase.content))))); 188 | 189 | describe('with YAML format', () => 190 | testCases.forEach(testCase => 191 | it(`should ${testCase.name}`, () => 192 | testEslint(testCase.expected, yaml.safeDump(testCase.content))))); 193 | }); 194 | -------------------------------------------------------------------------------- /src/special/gulp-load-plugins.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import lodash from 'lodash'; 3 | import minimatch from 'minimatch'; 4 | import traverse from 'babel-traverse'; 5 | 6 | import esParser from '../parser/es7'; 7 | import importDetector from '../detector/importDeclaration'; 8 | import requireDetector from '../detector/requireCallExpression'; 9 | 10 | function getPluginLookup(deps) { 11 | const patterns = ['gulp-*', 'gulp.*', '@*/gulp{-,.}*']; 12 | const lookup = lodash(deps) 13 | .filter(dep => 14 | patterns.some(pattern => minimatch(dep, pattern))) 15 | .map((dep) => { 16 | const isScoped = dep[0] === '@'; 17 | const scopedParts = dep.substring(1).split('/'); 18 | const scope = isScoped ? scopedParts[0] : ''; 19 | const plugin = isScoped ? scopedParts[1] : dep; 20 | const variableName = lodash.camelCase(plugin.substring('gulp-'.length)); 21 | const memberName = isScoped ? `.${scope}.${variableName}` : `.${variableName}`; 22 | return [memberName, dep]; 23 | }) 24 | .fromPairs() 25 | .value(); 26 | 27 | return lookup; 28 | } 29 | 30 | /** 31 | * Get the references to the variable in the path scope. 32 | * @example Within the path scope, returns references to `loadPlugins` variable. 33 | */ 34 | function getReferences(path, variableName) { 35 | const bindings = path.scope.getBinding(variableName); 36 | const references = bindings.referencePaths; 37 | return references; 38 | } 39 | 40 | /** 41 | * Get the variable name from the variable assigned declaration. 42 | * @example With code `$ = loadPlugins()` and `loadPlugins` as path, returns the string `$`. 43 | */ 44 | function getIdentifierVariableName(path) { 45 | if ( 46 | path.isIdentifier() && 47 | path.parentPath.isCallExpression() && 48 | path.parentPath.parentPath.isVariableDeclarator() 49 | ) { 50 | const variableName = path.parentPath.parentPath.node.id.name; 51 | return variableName; 52 | } 53 | 54 | return ''; 55 | } 56 | 57 | /** 58 | * Get the identifier references from imported/required load-plugin variable name. 59 | * @example With code `a = plugins(), b = plugins()`, returns uasge references to `a` and `b`. 60 | */ 61 | function getIdentifierReferences(path, loadPluginsVariableName) { 62 | const requireReferences = getReferences(path, loadPluginsVariableName); 63 | 64 | const identifierReferences = lodash(requireReferences) 65 | .map(getIdentifierVariableName) 66 | .filter() 67 | .map(identifierVariableName => getReferences(path, identifierVariableName)) 68 | .flatten() 69 | .value(); 70 | 71 | return identifierReferences; 72 | } 73 | 74 | /** 75 | * Get the package name from the identifier call path. 76 | * @example With code `$.jshint()` and `$` as path, returns `gulp-jshint` string. 77 | */ 78 | function getPackageName(content, pluginLookup, identifierPath) { 79 | let memberPath = identifierPath.parentPath; 80 | while (memberPath.isMemberExpression()) { 81 | const code = content.slice(identifierPath.node.end, memberPath.node.end); 82 | const pluginName = pluginLookup[code]; 83 | if (pluginName) { 84 | return pluginName; 85 | } 86 | 87 | memberPath = memberPath.parentPath; 88 | } 89 | 90 | return ''; 91 | } 92 | 93 | /** 94 | * Get the gulp packages found from the path. This is the entry for traverse. 95 | */ 96 | function check(content, deps, path) { 97 | if ( 98 | // Pattern: import plugins from 'gulp-load-plugins', $ = plugins(), $.jshint() 99 | importDetector(path.node)[0] === 'gulp-load-plugins' && 100 | path.isImportDeclaration() && 101 | path.get('specifiers')[0] && 102 | path.get('specifiers')[0].isImportDefaultSpecifier() && 103 | path.get('specifiers')[0].get('local').isIdentifier() 104 | ) { 105 | const importVariableName = path.get('specifiers')[0].get('local').node.name; 106 | const identifierReferences = getIdentifierReferences(path, importVariableName); 107 | const packageNames = identifierReferences.map(r => getPackageName(content, deps, r)); 108 | return packageNames; 109 | } else if ( 110 | 111 | // Pattern: plugins = require('gulp-load-plugins'), $ = plugins(), $.jshint() 112 | requireDetector(path.node)[0] === 'gulp-load-plugins' && 113 | path.isCallExpression() && 114 | path.parentPath.isVariableDeclarator() && 115 | path.parentPath.get('id').isIdentifier() 116 | ) { 117 | const requireVariableName = path.parentPath.get('id').node.name; 118 | const identifierReferences = getIdentifierReferences(path, requireVariableName); 119 | const packageNames = identifierReferences.map(r => getPackageName(content, deps, r)); 120 | return packageNames; 121 | } else if ( 122 | 123 | // Pattern: $ = require('gulp-load-plugins')(), $.jshint() 124 | requireDetector(path.node)[0] === 'gulp-load-plugins' && 125 | path.isCallExpression() && 126 | path.parentPath.isCallExpression() && 127 | path.parentPath.parentPath.isVariableDeclarator() && 128 | path.parentPath.parentPath.get('id').isIdentifier() 129 | ) { 130 | const requireVariableName = path.parentPath.parentPath.get('id').node.name; 131 | const identifierReferences = getReferences(path, requireVariableName); 132 | const packageNames = identifierReferences.map(r => getPackageName(content, deps, r)); 133 | return packageNames; 134 | } else if ( 135 | 136 | // Pattern: require('gulp-load-plugins')().thisPlugin() 137 | requireDetector(path.node)[0] === 'gulp-load-plugins' && 138 | path.isCallExpression() && 139 | path.parentPath.isCallExpression() && 140 | path.parentPath.parentPath.isMemberExpression() 141 | ) { 142 | const packageName = getPackageName(content, deps, path.parentPath); 143 | return [packageName]; 144 | } 145 | 146 | return []; 147 | } 148 | 149 | export default function parseGulpPlugins(content, filePath, deps, rootDir) { 150 | const resolvedPath = resolve(filePath); 151 | if ( 152 | resolvedPath !== resolve(rootDir, 'gulpfile.js') && 153 | resolvedPath !== resolve(rootDir, 'gulpfile.babel.js') 154 | ) { 155 | return []; 156 | } 157 | 158 | const pluginLookup = getPluginLookup(deps); 159 | const ast = esParser(content); 160 | const results = []; 161 | traverse(ast, { 162 | enter(path) { 163 | results.push(...check(content, pluginLookup, path)); 164 | }, 165 | }); 166 | 167 | return lodash(results) 168 | .filter() 169 | .uniq() 170 | .value(); 171 | } 172 | -------------------------------------------------------------------------------- /test/special/webpack.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | import 'should'; 4 | import path from 'path'; 5 | import fse from 'fs-extra'; 6 | import parse from '../../src/special/webpack'; 7 | 8 | const configFileNames = [ 9 | 'webpack.config.js', 10 | 'webpack.development.config.js', 11 | 'webpack.production.config.js', 12 | 'webpack.config.babel.js', 13 | 'webpack.prod.config.babel.js', 14 | ]; 15 | 16 | const testCases = [ 17 | { 18 | name: 'recognize single long-name webpack loader', 19 | deps: ['jade-loader'], 20 | module: { 21 | loaders: [ 22 | { test: /\.jade$/, loader: 'jade-loader' }, 23 | ], 24 | }, 25 | }, 26 | { 27 | name: 'recognize single short-name webpack loader', 28 | deps: ['jade-loader'], 29 | module: { 30 | loaders: [ 31 | { test: /\.jade$/, loader: 'jade' }, 32 | ], 33 | }, 34 | }, 35 | { 36 | name: 'recognize duplicated loader names', 37 | deps: ['jsx-loader'], 38 | module: { 39 | loaders: [ 40 | { test: /\.js$/, loader: 'jsx' }, 41 | { test: /\.jsx$/, loader: 'jsx' }, 42 | ], 43 | }, 44 | }, 45 | { 46 | name: 'recognize multiple webpack loaders concatenated with exclamation', 47 | deps: ['style-loader', 'css-loader'], 48 | module: { 49 | loaders: [ 50 | { test: /\.css$/, loader: 'style!css' }, 51 | ], 52 | }, 53 | }, 54 | { 55 | name: 'recognize multiple webpack loaders within loaders property', 56 | deps: ['style-loader', 'css-loader'], 57 | module: { 58 | loaders: [ 59 | { test: /\.css$/, loaders: ['style', 'css'] }, 60 | ], 61 | }, 62 | }, 63 | { 64 | name: 'recognize webpack loader with query parameters', 65 | deps: ['url-loader'], 66 | module: { 67 | loaders: [ 68 | { test: /\.png$/, loader: 'url-loader?mimetype=image/png' }, 69 | ], 70 | }, 71 | }, 72 | { 73 | name: 'recognize webpack loaders in preLoaders and postLoaders properties', 74 | deps: ['pre-webpack-loader', 'post-webpack-loader'], 75 | module: { 76 | preLoaders: [ 77 | { test: /\.pre$/, loader: 'pre' }, 78 | ], 79 | postLoaders: [ 80 | { test: /\.post$/, loader: 'post' }, 81 | ], 82 | }, 83 | }, 84 | { 85 | name: 'recognize webpack v2 loaders in module.rules.loaders', 86 | deps: ['style-loader'], 87 | module: { 88 | rules: [ 89 | { test: /\.css$/, loaders: ['style-loader'] }, 90 | ], 91 | }, 92 | }, 93 | { 94 | name: 'recognize webpack v2 loaders in module.rules.loader', 95 | deps: ['style-loader'], 96 | module: { 97 | rules: [ 98 | { test: /\.css$/, loader: 'style-loader' }, 99 | ], 100 | }, 101 | }, 102 | { 103 | name: 'recognize webpack v2 loaders in module.rules.loader (string array)', 104 | deps: ['style-loader'], 105 | module: { 106 | rules: [ 107 | { test: /\.css$/, loader: 'style-loader' }, 108 | ], 109 | }, 110 | }, 111 | { 112 | name: 'recognize webpack v2 loaders in module.rules.use', 113 | deps: ['style-loader'], 114 | module: { 115 | rules: [ 116 | { test: /\.css$/, use: 'style-loader' }, 117 | ], 118 | }, 119 | }, 120 | { 121 | name: 'recognize webpack v2 loaders in module.rules.use (string array)', 122 | deps: ['style-loader', 'css-loader'], 123 | module: { 124 | rules: [ 125 | { test: /\.css$/, use: ['style-loader', 'css-loader'] }, 126 | ], 127 | }, 128 | }, 129 | { 130 | name: 'recognize webpack v2 loaders in module.rules.use (object)', 131 | deps: ['style-loader'], 132 | module: { 133 | rules: [ 134 | { test: /\.css$/, use: { loader: 'style-loader' } }, 135 | ], 136 | }, 137 | }, 138 | { 139 | name: 'recognize webpack v2 loaders in module.rules.use (object array)', 140 | deps: ['style-loader', 'css-loader'], 141 | module: { 142 | rules: [ 143 | { test: /\.css$/, use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] }, 144 | ], 145 | }, 146 | }, 147 | { 148 | name: 'recognize webpack v2 loaders in module.rules.use (mixed array)', 149 | deps: ['style-loader', 'css-loader'], 150 | module: { 151 | rules: [ 152 | { test: /\.css$/, use: [{ loader: 'style-loader' }, 'css-loader'] }, 153 | ], 154 | }, 155 | }, 156 | { 157 | name: 'recognize webpack v2 loaders in module.rules.loader (object array)', 158 | deps: ['style-loader'], 159 | module: { 160 | rules: [ 161 | { test: /\.css$/, loader: [{ loader: 'style-loader' }] }, 162 | ], 163 | }, 164 | }, 165 | { 166 | name: 'handle invalid/unrecognised webpack v2 loaders', 167 | deps: [], 168 | module: { 169 | rules: [ 170 | { test: /\.css$/, loader: [{ loader: null }, 1] }, 171 | ], 172 | }, 173 | }, 174 | { 175 | name: 'handle invalid webpack config', 176 | deps: [], 177 | nomodule: true, 178 | }, 179 | ]; 180 | 181 | function random() { 182 | return Math.random().toString().substring(2); 183 | } 184 | 185 | async function getTempPath(filename, content) { 186 | const tempFolder = path.resolve(__dirname, `temp-${random()}`); 187 | const tempPath = path.resolve(tempFolder, filename); 188 | await fse.ensureDir(tempFolder); 189 | await fse.outputFile(tempPath, content); 190 | return tempPath; 191 | } 192 | 193 | async function removeTempFile(filepath) { 194 | const fileFolder = path.dirname(filepath); 195 | await fse.remove(filepath); 196 | await fse.remove(fileFolder); 197 | } 198 | 199 | async function testWebpack(filename, content, deps, expectedDeps) { 200 | const tempPath = await getTempPath(filename, content); 201 | const result = parse(content, tempPath, deps, __dirname); 202 | await removeTempFile(tempPath); 203 | Array.from(result).should.deepEqual(expectedDeps); 204 | } 205 | 206 | describe('webpack special parser', () => { 207 | it('should ignore when filename is not supported', () => { 208 | const result = parse('content', 'not-supported.txt', []); 209 | result.should.deepEqual([]); 210 | }); 211 | 212 | it('should recognize unused dependencies in webpack configuration', () => { 213 | const config = JSON.stringify({ module: testCases[0].module }); 214 | const content = `module.exports = ${config}`; 215 | const deps = testCases[0].deps.concat(['unused-loader']); 216 | return testWebpack('webpack.config.js', content, deps, testCases[0].deps); 217 | }); 218 | 219 | it('should handle require call to other modules', () => { 220 | const config = JSON.stringify({ module: testCases[0].module }); 221 | const content = `module.exports = ${config}\nrequire('webpack')`; 222 | return testWebpack('webpack.config.js', content, testCases[0].deps, testCases[0].deps); 223 | }); 224 | 225 | configFileNames.forEach(fileName => 226 | testCases.forEach(testCase => 227 | it(`should ${testCase.name} in configuration file ${fileName}`, () => { 228 | const config = JSON.stringify({ module: testCase.module }); 229 | const content = `module.exports = ${config}`; 230 | return testWebpack(fileName, content, testCase.deps, testCase.deps); 231 | }))); 232 | }); 233 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, after */ 2 | 3 | import 'should'; 4 | import path from 'path'; 5 | import cli from '../src/cli'; 6 | import testCases from './spec'; 7 | import { resolveShortPath } from './utils'; 8 | 9 | function makeArgv(module, options) { 10 | const testPath = path.resolve('test/fake_modules', module); 11 | const argv = [testPath]; 12 | 13 | if (options.json) { 14 | argv.push('--json'); 15 | } 16 | 17 | if (options.withoutDev) { 18 | argv.push('--dev=false'); 19 | } 20 | 21 | if (typeof options.ignoreBinPackage !== 'undefined') { 22 | argv.push(`--ignore-bin-package=${options.ignoreBinPackage}`); 23 | } 24 | 25 | if (options.ignoreMatches) { 26 | argv.push(`--ignores=${options.ignoreMatches.join(',')}`); 27 | } 28 | 29 | if (options.ignoreDirs) { 30 | argv.push(`--ignore-dirs=${options.ignoreDirs.join(',')}`); 31 | } 32 | 33 | if (options.detectors) { 34 | argv.push(`--detectors=${options.detectors.map(f => f.name).join(',')}`); 35 | } 36 | 37 | if (options.argv && options.argv.length) { 38 | argv.push(...options.argv); 39 | } 40 | 41 | if (options.skipMissing !== undefined) { 42 | argv.push(`--skip-missing=${options.skipMissing}`); 43 | } 44 | 45 | return argv; 46 | } 47 | 48 | function testCli(argv) { 49 | let log = ''; 50 | let error = ''; 51 | 52 | return new Promise(resolve => 53 | cli( 54 | argv, 55 | (data) => { log = data; }, 56 | (data) => { error = data; }, 57 | exitCode => resolve({ 58 | log, 59 | error, 60 | exitCode, 61 | logs: log.split('\n').filter(line => line), 62 | errors: error.split('\n').filter(line => line), 63 | }))); 64 | } 65 | 66 | describe('depcheck command line', () => { 67 | testCases.forEach((testCase) => { 68 | const run = testCase.only === 'cli' ? it.only : it; 69 | const options = Object.assign({ json: true }, testCase.options); 70 | run(`should ${testCase.name}`, () => 71 | testCli(makeArgv(testCase.module, options)) 72 | .then(({ log, error, exitCode }) => { 73 | const actual = JSON.parse(log); 74 | const expected = testCase.expected; 75 | 76 | actual.dependencies.should.eql(expected.dependencies); 77 | actual.devDependencies.should.eql(expected.devDependencies); 78 | actual.missing.should.eql(resolveShortPath(expected.missing, testCase.module)); 79 | actual.using.should.eql(resolveShortPath(expected.using, testCase.module)); 80 | 81 | error.should.be.empty(); 82 | exitCode.should.equal(0); // JSON output always return 0 83 | })); 84 | }); 85 | 86 | it('should output error when folder is not a package', () => 87 | testCli([__dirname]) 88 | .then(({ log, error, exitCode }) => { 89 | error.should.containEql(__dirname) 90 | .and.containEql('not contain') 91 | .and.containEql('package.json'); 92 | 93 | log.should.be.empty(); 94 | exitCode.should.equal(-1); 95 | })); 96 | 97 | it('should output error when folder not exists', () => 98 | testCli(['./not/exist/folder']) 99 | .then(({ log, error, exitCode }) => { 100 | error.should.containEql('/not/exist/folder').and.containEql('not exist'); 101 | log.should.be.empty(); 102 | exitCode.should.equal(-1); 103 | })); 104 | 105 | it('should output call stack for invalid files in JSON view', () => 106 | testCli(makeArgv('bad_js', { json: true })) 107 | .then(({ log, error, exitCode }) => { 108 | const json = JSON.parse(log); 109 | json.should.have.properties([ 110 | 'dependencies', 111 | 'devDependencies', 112 | 'invalidFiles', 113 | 'invalidDirs', 114 | ]); 115 | 116 | const badJsPath = path.resolve(__dirname, './fake_modules/bad_js/index.js'); 117 | json.invalidFiles.should.have.property(badJsPath) 118 | .startWith('SyntaxError: Unexpected token') 119 | .and.containEql('\n at '); // call stack information 120 | 121 | error.should.be.empty(); 122 | exitCode.should.equal(0); 123 | })); 124 | 125 | it('should output no depcheck issue when happen', () => 126 | testCli([path.resolve(__dirname, './fake_modules/good')]) 127 | .then(({ log, error, exitCode }) => { 128 | log.should.equal('No depcheck issue'); 129 | error.should.be.empty(); 130 | exitCode.should.equal(0); 131 | })); 132 | 133 | it('should output unused dependencies when happen', () => 134 | testCli(makeArgv('bad', {})) 135 | .then(({ logs, error, exitCode }) => { 136 | logs.should.have.length(2); 137 | logs[0].should.equal('Unused dependencies'); 138 | logs[1].should.containEql('optimist'); 139 | 140 | error.should.be.empty(); 141 | exitCode.should.equal(-1); 142 | })); 143 | 144 | it('should output unused devDependencies when happen', () => 145 | testCli(makeArgv('dev', {})) 146 | .then(({ logs, error, exitCode }) => { 147 | logs.should.have.length(2); 148 | logs[0].should.equal('Unused devDependencies'); 149 | logs[1].should.containEql('unused-dev-dep'); 150 | 151 | error.should.be.empty(); 152 | exitCode.should.equal(-1); 153 | })); 154 | 155 | it('should recognize JSX file even only pass jsx parser and require detector', () => 156 | testCli(makeArgv('jsx', { 157 | argv: ['--parsers="*.jsx:jsx"', '--dectors=requireCallExpression'], 158 | })) 159 | .then(({ logs, error, exitCode }) => { 160 | logs.should.have.length(2); 161 | logs[0].should.equal('Unused dependencies'); 162 | logs[1].should.containEql('react'); 163 | 164 | error.should.be.empty(); 165 | exitCode.should.equal(-1); 166 | })); 167 | 168 | it('should not recognize JSX file when not pass jsx parser', () => 169 | testCli(makeArgv('jsx', { 170 | argv: ['--parsers="*.jsx:es6"'], 171 | })) 172 | .then(({ logs, error, exitCode }) => { 173 | logs.should.have.length(2); 174 | logs[0].should.equal('Unused dependencies'); 175 | logs[1].should.containEql('react'); 176 | 177 | error.should.be.empty(); 178 | exitCode.should.equal(-1); 179 | })); 180 | 181 | it('should not recognize JSX file when not enable require detector', () => 182 | testCli(makeArgv('jsx', { 183 | argv: ['--detectors=importDeclaration'], 184 | })) 185 | .then(({ logs, error, exitCode }) => { 186 | logs.should.have.length(2); 187 | logs[0].should.equal('Unused dependencies'); 188 | logs[1].should.containEql('react'); 189 | 190 | error.should.be.empty(); 191 | exitCode.should.equal(-1); 192 | })); 193 | 194 | it('should find dependencies with special parser', () => 195 | testCli(makeArgv('eslint_config', { 196 | argv: ['--specials=eslint'], 197 | })) 198 | .then(({ logs, error, exitCode }) => { 199 | logs.should.have.length(2); 200 | logs[0].should.equal('Unused devDependencies'); 201 | logs[1].should.containEql('eslint-config-unused'); 202 | 203 | error.should.be.empty(); 204 | exitCode.should.equal(-1); 205 | })); 206 | 207 | describe('without specified directory', () => { 208 | let originalCwd; 209 | 210 | before(() => { 211 | originalCwd = process.cwd; 212 | process.cwd = () => '/not/exist'; 213 | }); 214 | 215 | it('should default to the current directory', () => 216 | testCli([]) 217 | .then(({ log, error, exitCode }) => { 218 | error.should.containEql('not exist'); 219 | log.should.be.empty(); 220 | exitCode.should.equal(-1); 221 | })); 222 | 223 | after(() => { process.cwd = originalCwd; }); 224 | }); 225 | }); 226 | -------------------------------------------------------------------------------- /src/check.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import lodash from 'lodash'; 4 | import walkdir from 'walkdir'; 5 | import minimatch from 'minimatch'; 6 | import builtInModules from 'builtin-modules'; 7 | import requirePackageName from 'require-package-name'; 8 | import { readJSON } from './utils'; 9 | 10 | function isModule(dir) { 11 | try { 12 | readJSON(path.resolve(dir, 'package.json')); 13 | return true; 14 | } catch (error) { 15 | return false; 16 | } 17 | } 18 | 19 | function mergeBuckets(object1, object2) { 20 | return lodash.mergeWith(object1, object2, (value1, value2) => { 21 | const array1 = value1 || []; 22 | const array2 = value2 || []; 23 | return array1.concat(array2); 24 | }); 25 | } 26 | 27 | function detect(detectors, node) { 28 | return lodash(detectors) 29 | .map((detector) => { 30 | try { 31 | return detector(node); 32 | } catch (error) { 33 | return []; 34 | } 35 | }) 36 | .flatten() 37 | .value(); 38 | } 39 | 40 | // fix for node.js <= 3, it throws TypeError when value type invalid in weak set 41 | function hasVisited(ast, visited) { 42 | try { 43 | return visited.has(ast); 44 | } catch (e) { 45 | return false; 46 | } 47 | } 48 | 49 | function recursive(ast, visited) { 50 | if (!ast || hasVisited(ast, visited)) { 51 | return []; 52 | } else if (lodash.isArray(ast)) { 53 | return lodash(ast) 54 | .map(node => recursive(node, visited)) 55 | .flatten() 56 | .value(); 57 | } else if (ast.type) { 58 | visited.add(ast); 59 | return lodash(ast) 60 | .keys() 61 | .filter(key => key !== 'tokens' && key !== 'comments') 62 | .map(key => recursive(ast[key], visited)) 63 | .flatten() 64 | .concat(ast) 65 | .value(); 66 | } 67 | 68 | return []; 69 | } 70 | 71 | function getNodes(ast) { 72 | const visited = new WeakSet(); 73 | const nodes = recursive(ast, visited); 74 | return nodes; 75 | } 76 | 77 | function discoverPropertyDep(rootDir, deps, property, depName) { 78 | try { 79 | const file = path.resolve(rootDir, 'node_modules', depName, 'package.json'); 80 | const metadata = readJSON(file); 81 | const propertyDeps = Object.keys(metadata[property] || {}); 82 | return lodash.intersection(deps, propertyDeps); 83 | } catch (error) { 84 | return []; 85 | } 86 | } 87 | 88 | function getDependencies(dir, filename, deps, parser, detectors) { 89 | return new Promise((resolve, reject) => { 90 | fs.readFile(filename, 'utf8', (error, content) => { 91 | if (error) { 92 | reject(error); 93 | } 94 | 95 | try { 96 | resolve(parser(content, filename, deps, dir)); 97 | } catch (syntaxError) { 98 | reject(syntaxError); 99 | } 100 | }); 101 | }).then((ast) => { 102 | // when parser returns string array, skip detector step and treat them as dependencies. 103 | const dependencies = lodash.isArray(ast) && ast.every(lodash.isString) 104 | ? ast 105 | : lodash(getNodes(ast)) 106 | .map(node => detect(detectors, node)) 107 | .flatten() 108 | .uniq() 109 | .map(requirePackageName) 110 | .value(); 111 | 112 | const discover = lodash.partial(discoverPropertyDep, dir, deps); 113 | const discoverPeerDeps = lodash.partial(discover, 'peerDependencies'); 114 | const discoverOptionalDeps = lodash.partial(discover, 'optionalDependencies'); 115 | const peerDeps = lodash(dependencies) 116 | .map(discoverPeerDeps) 117 | .flatten() 118 | .value(); 119 | const optionalDeps = lodash(dependencies) 120 | .map(discoverOptionalDeps) 121 | .flatten() 122 | .value(); 123 | 124 | return dependencies.concat(peerDeps).concat(optionalDeps); 125 | }); 126 | } 127 | 128 | function checkFile(dir, filename, deps, parsers, detectors) { 129 | const basename = path.basename(filename); 130 | const targets = lodash(parsers) 131 | .keys() 132 | .filter(glob => minimatch(basename, glob, { dot: true })) 133 | .map(key => parsers[key]) 134 | .flatten() 135 | .value(); 136 | 137 | return targets.map(parser => 138 | getDependencies(dir, filename, deps, parser, detectors) 139 | .then(using => ({ 140 | using: { 141 | [filename]: lodash(using) 142 | .filter(dep => dep && dep !== '.' && dep !== '..') // TODO why need check? 143 | .filter(dep => !lodash.includes(builtInModules, dep)) 144 | .uniq() 145 | .value(), 146 | }, 147 | }), error => ({ 148 | invalidFiles: { 149 | [filename]: error, 150 | }, 151 | }))); 152 | } 153 | 154 | function checkDirectory(dir, rootDir, ignoreDirs, deps, parsers, detectors) { 155 | return new Promise((resolve) => { 156 | const promises = []; 157 | const finder = walkdir(dir, { no_recurse: true }); 158 | 159 | finder.on('directory', subdir => 160 | (ignoreDirs.indexOf(path.basename(subdir)) === -1 && !isModule(subdir) 161 | ? promises.push(checkDirectory(subdir, rootDir, ignoreDirs, deps, parsers, detectors)) 162 | : null)); 163 | 164 | finder.on('file', filename => 165 | promises.push(...checkFile(rootDir, filename, deps, parsers, detectors))); 166 | 167 | finder.on('error', (dirPath, error) => 168 | promises.push(Promise.resolve({ 169 | invalidDirs: { 170 | [dirPath]: error, 171 | }, 172 | }))); 173 | 174 | finder.on('end', () => 175 | resolve(Promise.all(promises).then(results => 176 | results.reduce((obj, current) => ({ 177 | using: mergeBuckets(obj.using, current.using || {}), 178 | invalidFiles: Object.assign(obj.invalidFiles, current.invalidFiles), 179 | invalidDirs: Object.assign(obj.invalidDirs, current.invalidDirs), 180 | }), { 181 | using: {}, 182 | invalidFiles: {}, 183 | invalidDirs: {}, 184 | })))); 185 | }); 186 | } 187 | 188 | function buildResult(result, deps, devDeps, peerDeps, optionalDeps, skipMissing) { 189 | const usingDepsLookup = lodash(result.using) 190 | // { f1:[d1,d2,d3], f2:[d2,d3,d4] } 191 | .toPairs() 192 | // [ [f1,[d1,d2,d3]], [f2,[d2,d3,d4]] ] 193 | .map(([file, dep]) => [dep, lodash.times(dep.length, () => file)]) 194 | // [ [ [d1,d2,d3],[f1,f1,f1] ], [ [d2,d3,d4],[f2,f2,f2] ] ] 195 | .map(pairs => lodash.zip(...pairs)) 196 | // [ [ [d1,f1],[d2,f1],[d3,f1] ], [ [d2,f2],[d3,f2],[d4,f2]] ] 197 | .flatten() 198 | // [ [d1,f1], [d2,f1], [d3,f1], [d2,f2], [d3,f2], [d4,f2] ] 199 | .groupBy(([dep]) => dep) 200 | // { d1:[ [d1,f1] ], d2:[ [d2,f1],[d2,f2] ], d3:[ [d3,f1],[d3,f2] ], d4:[ [d4,f2] ] } 201 | .mapValues(pairs => pairs.map(lodash.last)) 202 | // { d1:[ f1 ], d2:[ f1,f2 ], d3:[ f1,f2 ], d4:[ f2 ] } 203 | .value(); 204 | 205 | const usingDeps = Object.keys(usingDepsLookup); 206 | const allDeps = deps.concat(devDeps).concat(peerDeps).concat(optionalDeps); 207 | const missingDeps = lodash.difference(usingDeps, allDeps); 208 | 209 | const missingDepsLookup = skipMissing 210 | ? [] 211 | : lodash(missingDeps) 212 | .map(missingDep => [missingDep, usingDepsLookup[missingDep]]) 213 | .fromPairs() 214 | .value(); 215 | 216 | return { 217 | dependencies: lodash.difference(deps, usingDeps), 218 | devDependencies: lodash.difference(devDeps, usingDeps), 219 | missing: missingDepsLookup, 220 | using: usingDepsLookup, 221 | invalidFiles: result.invalidFiles, 222 | invalidDirs: result.invalidDirs, 223 | }; 224 | } 225 | 226 | export default function check({ 227 | rootDir, 228 | ignoreDirs, 229 | skipMissing, 230 | deps, 231 | devDeps, 232 | peerDeps, 233 | optionalDeps, 234 | parsers, 235 | detectors, 236 | }) { 237 | const allDeps = lodash.union(deps, devDeps); 238 | return checkDirectory(rootDir, rootDir, ignoreDirs, allDeps, parsers, detectors) 239 | .then(result => buildResult(result, deps, devDeps, peerDeps, optionalDeps, skipMissing)); 240 | } 241 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, after */ 2 | 3 | import 'should'; 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import { platform } from 'os'; 7 | 8 | import testCases from './spec'; 9 | import depcheck from '../src/index'; 10 | import { resolveShortPath } from './utils'; 11 | 12 | import { 13 | full as importListParser, 14 | lite as importListParserLite, 15 | } from './fake_parsers/importList'; 16 | 17 | import exceptionParser from './fake_parsers/exception'; 18 | import { multipleParserA, multipleParserB } from './fake_parsers/multiple'; 19 | 20 | import exceptionDetector from './fake_detectors/exception'; 21 | import dependDetector from './fake_detectors/dependCallExpression'; 22 | 23 | function check(module, options) { 24 | return new Promise(resolve => 25 | depcheck( 26 | path.resolve(__dirname, 'fake_modules', module), 27 | options, 28 | resolve)); 29 | } 30 | 31 | describe('depcheck', () => { 32 | testCases.forEach((testCase) => { 33 | const run = testCase.only === 'index' ? it.only : it; 34 | run(`should ${testCase.name}`, () => 35 | check(testCase.module, testCase.options).then((result) => { 36 | const expected = testCase.expected; 37 | result.dependencies.should.eql(expected.dependencies); 38 | result.devDependencies.should.eql(expected.devDependencies); 39 | result.missing.should.eql(resolveShortPath(expected.missing, testCase.module)); 40 | result.using.should.eql(resolveShortPath(expected.using, testCase.module)); 41 | })); 42 | }); 43 | 44 | it('should ignore bad javascript', () => 45 | check('bad_js', {}).then((unused) => { 46 | unused.dependencies.should.deepEqual(['optimist']); 47 | 48 | const invalidFiles = Object.keys(unused.invalidFiles); 49 | invalidFiles.should.have.length(1); 50 | invalidFiles[0].should.endWith( 51 | path.join('/test/fake_modules/bad_js/index.js')); 52 | 53 | const error = unused.invalidFiles[invalidFiles[0]]; 54 | error.should.be.instanceof(SyntaxError); 55 | })); 56 | 57 | it('should allow dynamic package metadata', () => 58 | check('bad', { 59 | package: { 60 | dependencies: { 61 | optimist: '~0.6.0', 62 | express: '^4.0.0', 63 | }, 64 | }, 65 | }).then((unused) => { 66 | unused.dependencies.should.deepEqual(['optimist', 'express']); 67 | })); 68 | 69 | function testAccessUnreadableDirectory( 70 | module, unreadable, unusedDeps, unusedDevDeps) { 71 | if (platform() === 'win32') { 72 | return; // cannot test permission cases in Windows 73 | } 74 | 75 | const unreadablePath = 76 | path.resolve(__dirname, 'fake_modules', module, unreadable); 77 | 78 | before(done => fs.mkdir(unreadablePath, '0000', done)); 79 | 80 | it('should capture error', () => 81 | check(module, {}).then((unused) => { 82 | unused.dependencies.should.deepEqual(unusedDeps); 83 | unused.devDependencies.should.deepEqual(unusedDevDeps); 84 | 85 | const invalidDirs = Object.keys(unused.invalidDirs); 86 | invalidDirs.should.deepEqual([unreadablePath]); 87 | 88 | const error = unused.invalidDirs[invalidDirs[0]]; 89 | error.should.be.instanceof(Error); 90 | error.toString().should.containEql('EACCES'); 91 | })); 92 | 93 | after(done => 94 | fs.chmod(unreadablePath, '0700', error => 95 | (error ? done(error) : fs.rmdir(unreadablePath, done)))); 96 | } 97 | 98 | describe('access unreadable directory', () => 99 | testAccessUnreadableDirectory( 100 | 'unreadable', 101 | 'unreadable', 102 | ['unreadable'], 103 | [])); 104 | 105 | describe('access deep unreadable directory', () => 106 | testAccessUnreadableDirectory( 107 | 'unreadable_deep', 108 | 'deep/nested/unreadable', 109 | [], 110 | [])); 111 | 112 | function testAccessUnreadableFile( 113 | module, unreadable, unusedDeps, unusedDevDeps) { 114 | if (platform() === 'win32') { 115 | return; // cannot test permission cases in Windows 116 | } 117 | 118 | const unreadablePath = 119 | path.resolve(__dirname, 'fake_modules', module, unreadable); 120 | 121 | before(done => fs.writeFile(unreadablePath, '', { mode: 0 }, done)); 122 | 123 | it('should capture error', () => 124 | check(module, {}).then((unused) => { 125 | unused.dependencies.should.deepEqual(unusedDeps); 126 | unused.devDependencies.should.deepEqual(unusedDevDeps); 127 | 128 | const invalidFiles = Object.keys(unused.invalidFiles); 129 | invalidFiles.should.deepEqual([unreadablePath]); 130 | 131 | const error = unused.invalidFiles[invalidFiles[0]]; 132 | error.should.be.instanceof(Error); 133 | error.toString().should.containEql('EACCES'); 134 | })); 135 | 136 | after(done => 137 | fs.chmod(unreadablePath, '0700', error => 138 | (error ? done(error) : fs.unlink(unreadablePath, done)))); 139 | } 140 | 141 | describe('access unreadable file', () => 142 | testAccessUnreadableFile( 143 | 'unreadable', 144 | 'unreadable.js', 145 | ['unreadable'], 146 | [])); 147 | 148 | function testCustomPluggableComponents(module, options) { 149 | return check(module, options).then((unused) => { 150 | unused.dependencies.should.deepEqual([]); 151 | unused.devDependencies.should.deepEqual([]); 152 | 153 | Object.keys(unused.invalidFiles).should.have.length(0); 154 | Object.keys(unused.invalidDirs).should.have.length(0); 155 | }); 156 | } 157 | 158 | it('should work fine even a customer parser throws exceptions', () => 159 | testCustomPluggableComponents('good', { 160 | detectors: [ 161 | depcheck.detector.requireCallExpression, 162 | exceptionDetector, 163 | ], 164 | })); 165 | 166 | it('should use custom parsers to generate AST', () => 167 | testCustomPluggableComponents('import_list', { 168 | parsers: { 169 | '*.txt': importListParser, 170 | }, 171 | })); 172 | 173 | it('should handle the returned string array as dependent packages', () => 174 | testCustomPluggableComponents('import_list', { 175 | parsers: { 176 | '*.txt': importListParserLite, 177 | }, 178 | detectors: [ 179 | // the detector step is skipped because parser returns string array 180 | ], 181 | })); 182 | 183 | it('should discover peer dependencies for the parser returning string array case', () => 184 | testCustomPluggableComponents('import_list_peer', { 185 | parsers: { 186 | '*.txt': importListParserLite, 187 | }, 188 | detectors: [ 189 | // the detector step is skipped because parser returns string array 190 | ], 191 | })); 192 | 193 | it('should support multiple parsers to generate ASTs', () => 194 | testCustomPluggableComponents('multiple_parsers', { 195 | parsers: { 196 | '*.csv': [ 197 | multipleParserA, 198 | multipleParserB, 199 | ], 200 | }, 201 | })); 202 | 203 | it('should generate ASTs when multiple globs match filename', () => 204 | testCustomPluggableComponents('multiple_parsers', { 205 | parsers: { 206 | '*.csv': multipleParserA, 207 | 'index.*': multipleParserB, 208 | }, 209 | })); 210 | 211 | it('should use custom detector to find dependencies', () => 212 | testCustomPluggableComponents('depend', { 213 | detectors: [ 214 | dependDetector, 215 | ], 216 | })); 217 | 218 | it('should handle other parsers even one throws exception', () => 219 | check('import_list', { 220 | parsers: { 221 | '*.txt': [ 222 | importListParser, 223 | exceptionParser, 224 | ], 225 | }, 226 | }).then((unused) => { 227 | unused.dependencies.should.deepEqual([]); 228 | unused.devDependencies.should.deepEqual([]); 229 | 230 | Object.keys(unused.invalidDirs).should.have.length(0); 231 | 232 | Object.keys(unused.invalidFiles).should.have.length(1); 233 | Object.keys(unused.invalidFiles)[0].should.endWith( 234 | path.join('/test/fake_modules/import_list/index.txt')); 235 | })); 236 | }); 237 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # depcheck 2 | 3 | Depcheck is a tool for analyzing the dependencies in a project to see: how each dependency is used, which dependencies are useless, and which dependencies are missing from `package.json`. 4 | 5 | ## Status 6 | 7 | [![Build Status](https://travis-ci.org/depcheck/depcheck.svg?branch=master)](https://travis-ci.org/depcheck/depcheck) 8 | [![Build status](https://ci.appveyor.com/api/projects/status/xbooh370dinuyi0y/branch/master?svg=true)](https://ci.appveyor.com/project/lijunle/depcheck/branch/master) 9 | [![codecov.io](https://codecov.io/github/depcheck/depcheck/coverage.svg?branch=master)](https://codecov.io/github/depcheck/depcheck?branch=master) 10 | 11 | [![dependencies Status](https://david-dm.org/depcheck/depcheck/status.svg)](https://david-dm.org/depcheck/depcheck) 12 | [![devDependencies Status](https://david-dm.org/depcheck/depcheck/dev-status.svg)](https://david-dm.org/depcheck/depcheck?type=dev) 13 | 14 | ## Installation 15 | 16 | ``` 17 | npm install -g depcheck 18 | ``` 19 | 20 | *Notice:* depcheck needs node.js >= 4. 21 | 22 | ## Syntax Support 23 | 24 | Depcheck not only recognizes the dependencies in JavaScript files, but also supports these syntaxes: 25 | 26 | - JavaScript (ES5, ES6 and ES7) 27 | - [React JSX](http://facebook.github.io/react/docs/jsx-in-depth.html) 28 | - [CoffeeScript](http://coffeescript.org/) 29 | - [Typescript](http://www.typescriptlang.org/) (with `typescript` dependency) 30 | - [SASS and SCSS](http://sass-lang.com/) (with `node-sass` dependency) 31 | 32 | To get the syntax support by external dependency, please install the corresponding package explicitly. For example, for Typescript user, install depcheck with `typescript` package: 33 | 34 | ``` 35 | npm install -g depcheck typescript 36 | ``` 37 | 38 | ## Special 39 | 40 | The *special* component is used to recognize the dependencies that are not generally used in the above syntax files. The following scenarios are supported by specials: 41 | 42 | - `bin` - Dependencies used in npm commands, Travis scripts or other CI scripts 43 | - `eslint` - [ESLint](https://www.npmjs.com/package/eslint) configuration presets, parsers and plugins 44 | - `webpack` - [Webpack](https://www.npmjs.com/package/webpack) loaders 45 | - `babel` - [Babel](https://www.npmjs.com/package/babel) presets and plugins 46 | - [Grunt](https://www.npmjs.com/package/grunt) plugins 47 | - `feross-standard` - [Feross standard](https://www.npmjs.com/package/standard) format parser 48 | - `mocha` - [Mocha](https://www.npmjs.com/package/mocha) explicit required dependencies 49 | - `commitizen` - [Commitizen](https://www.npmjs.com/package/commitizen) configuration adaptor 50 | - `gulp-load-plugins` - [Gulp-load-plugins](https://www.npmjs.com/package/gulp-load-plugins) lazy loaded plugins 51 | 52 | The logic of a special is not perfect. There might be [false alerts](#false-alert). If this happens, please open an issue for us. 53 | 54 | ## Usage 55 | 56 | ``` 57 | depcheck [directory] [arguments] 58 | ``` 59 | 60 | The `directory` argument is the root directory of your project (where the `package.json` file is). If unspecified, defaults to current directory. 61 | 62 | All of the arguments are optional: 63 | 64 | `--ignore-bin-package=[true|false]`: A flag to indicate if depcheck ignores the packages containing bin entry. The default value is `true`. 65 | 66 | `--skip-missing=[true|false]`: A flag to indicate if depcheck skips calculation of missing dependencies. The default value is `false`. 67 | 68 | `--json`: Output results in JSON. When not specified, depcheck outputs in human friendly format. 69 | 70 | `--ignores`: A comma separated array containing package names to ignore. It can be glob expressions. Example, `--ignores="eslint,babel-*"`. 71 | 72 | `--ignore-dirs`: A comma separated array containing directory names to ignore. Example, `--ignore-dirs=dist,coverage`. 73 | 74 | `--help`: Show the help message. 75 | 76 | `--parsers`, `--detectors` and `--specials`: These arguments are for advanced usage. They provide an easy way to customize the file parser and dependency detection. Check [the pluggable design document](https://github.com/depcheck/depcheck/blob/master/doc/pluggable-design.md) for more information. 77 | 78 | ### Deprecated arguments 79 | 80 | The following arguments are deprecated and will be removed in next major version: 81 | 82 | `--dev=[true|false]`: *[DEPRECATED]* It leads a wrong result for missing dependencies when it is `false`. This option will be enforced to `true` in next major version. The corresponding API option `withoutDev` is deprecated too. 83 | 84 | ## API 85 | 86 | Similar options are provided to `depcheck` function for programming. 87 | 88 | ```js 89 | import depcheck from 'depcheck'; 90 | 91 | const options = { 92 | withoutDev: false, // [DEPRECATED] check against devDependencies 93 | ignoreBinPackage: false, // ignore the packages with bin entry 94 | skipMissing: false, // skip calculation of missing dependencies 95 | ignoreDirs: [ // folder with these names will be ignored 96 | 'sandbox', 97 | 'dist', 98 | 'bower_components' 99 | ], 100 | ignoreMatches: [ // ignore dependencies that matches these globs 101 | 'grunt-*' 102 | ], 103 | parsers: { // the target parsers 104 | '*.js': depcheck.parser.es6, 105 | '*.jsx': depcheck.parser.jsx 106 | }, 107 | detectors: [ // the target detectors 108 | depcheck.detector.requireCallExpression, 109 | depcheck.detector.importDeclaration 110 | ], 111 | specials: [ // the target special parsers 112 | depcheck.special.eslint, 113 | depcheck.special.webpack 114 | ], 115 | }; 116 | 117 | depcheck('/path/to/your/project', options, (unused) => { 118 | console.log(unused.dependencies); // an array containing the unused dependencies 119 | console.log(unused.devDependencies); // an array containing the unused devDependencies 120 | console.log(unused.missing); // a lookup containing the dependencies missing in `package.json` and where they are used 121 | console.log(unused.using); // a lookup indicating each dependency is used by which files 122 | console.log(unused.invalidFiles); // files that cannot access or parse 123 | console.log(unused.invalidDirs); // directories that cannot access 124 | }); 125 | ``` 126 | 127 | ## Example 128 | 129 | The following example checks the dependencies under `/path/to/my/project` folder. 130 | 131 | ```sh 132 | $> depcheck /path/to/my/project 133 | Unused dependencies 134 | * underscore 135 | Unused devDependencies 136 | * jasmine 137 | Missing dependencies 138 | * lodash 139 | ``` 140 | 141 | It figures out: 142 | 143 | - The dependency `underscore` is declared in the `package.json` file, but not used by any code. 144 | - The devDependency `jasmine` is declared in the `package.json` file, but not used by any code. 145 | - The dependency `lodash` is used somewhere in the code, but not declared in the `package.json` file. 146 | 147 | Please note that, if a subfolder has a `package.json` file, it is considered another project and should be checked with another depcheck command. 148 | 149 | The following example checks the same project, however, outputs as a JSON blob. Depcheck's JSON output is in one single line for easy pipe and computation. The [`json`](https://www.npmjs.com/package/json) command after the pipe is a node.js program to beautify the output. 150 | 151 | ```js 152 | $> depcheck /path/to/my/project --json | json 153 | { 154 | "dependencies": [ 155 | "underscore" 156 | ], 157 | "devDependencies": [ 158 | "jasmine" 159 | ], 160 | "missing": { 161 | "lodash": [ 162 | "/path/to/my/project/file.using.lodash.js" 163 | ] 164 | }, 165 | "using": { 166 | "react": [ 167 | "/path/to/my/project/file.using.react.jsx", 168 | "/path/to/my/project/another.file.using.react.jsx" 169 | ], 170 | "lodash": [ 171 | "/path/to/my/project/file.using.lodash.js" 172 | ] 173 | }, 174 | "invalidFiles": { 175 | "/path/to/my/project/file.having.syntax.error.js": "SyntaxError: " 176 | }, 177 | "invalidDirs": { 178 | "/path/to/my/project/folder/without/permission": "Error: EACCES, " 179 | } 180 | } 181 | ``` 182 | 183 | - The `dependencies`, `devDependencies` and `missing` properties have the same meanings in the previous example. 184 | - The `using` property is a lookup indicating each dependency is used by which files. 185 | - The value of `missing` and `using` lookup is an array. It means the dependency may be used by many files. 186 | - The `invalidFiles` property contains the files having syntax error or permission error. The value is the error details. However, only one error is stored in the lookup. 187 | - The `invalidDirs` property contains the directories having permission error. The value is the error details. 188 | 189 | ## False Alert 190 | 191 | Depcheck just walks through all files and tries to find the dependencies according to some predefined rules. However, the predefined rules may not enough or even be wrong. 192 | 193 | There may be some cases in which a dependency is being used but is reported as unused, or a dependency is not used but is reported as missing. These are *false alert* situations. 194 | 195 | If you find that depcheck is reporting a false alert, please [open an issue](https://github.com/depcheck/depcheck/issues/new) with the following information to let us know: 196 | 197 | - The output from `depcheck --json` command. Beautified JSON is better. 198 | - Which dependencies are considered as false alert? 199 | - How are you using those dependencies, what do the files look like? 200 | 201 | ## Changelog 202 | 203 | We use the [Github release page](https://github.com/depcheck/depcheck/releases) to manage changelog. 204 | 205 | ## License 206 | 207 | MIT License. 208 | -------------------------------------------------------------------------------- /doc/pluggable-design.md: -------------------------------------------------------------------------------- 1 | # Pluggable design 2 | 3 | Pluggable design is to make the depcheck flexible. The design allows user to customize the syntax parsing, dependencies package detection. 4 | 5 | Here is the normal depcheck workflow: 6 | 7 | ``` 8 | walk files under directory -> parse file -> detect packages it used 9 | ``` 10 | 11 | There are three parts in the workflow: *walk files*, *parse file* and *detect packages*. The second and third part can be customized with **parser** and **detection**. 12 | 13 | ## Parser 14 | 15 | Parser is a function to parse file content to its abstract syntax tree (aka, AST). 16 | 17 | Depcheck ships the default JavaScript file parser as `depcheck.parser.es6` and the JSX file parser as `depcheck.parser.jsx`. 18 | 19 | ### Use Parser From API 20 | 21 | Depcheck API accepts `parsers` property to specify the parsers. The syntax looks like: 22 | 23 | ```js 24 | var opts = { 25 | parsers: { 26 | '*.js': depcheck.parser.es6, 27 | '*.jsx': depcheck.parser.jsx, 28 | '*.json': [ 29 | depcheck.parser.json, 30 | customJsonParser 31 | ] 32 | } 33 | }; 34 | ``` 35 | 36 | The `parsers` option accepts an object. The object key is a glob pattern. The value is the corresponding parser function or parser function array. Only the file whose name matches the glob pattern will be converted to ASTs by the corresponding parsers. 37 | 38 | When a file name matches multiple glob patterns, or the glob corresponds to a parser array, the file will be handled by these parsers one by one to generate multiple ASTs. 39 | 40 | Here is the default `parsers` option value when user not specify explicitly: 41 | 42 | ```js 43 | var opts = { 44 | parsers: { 45 | '*.js': depcheck.parser.es6, 46 | '*.jsx': depcheck.parser.jsx 47 | } 48 | }; 49 | ``` 50 | 51 | ### Use Parser From CLI 52 | 53 | From CLI, user can only specify the out-of-box parsers. The CLI evaluates the value from `--parsers` argument, then convert it into *glob-parser* pairs. The syntax of `--parsers` argument looks like this: 54 | 55 | ``` 56 | --parsers="*.js:es6,*.jsx:jsx,*.json:json1&json2" 57 | ``` 58 | 59 | The quote mark (`"`) wrapping the value is to avoid the start mark (`*`) be parsed by CLI. 60 | 61 | As shown from the example, each *glob-parser* pair is concatenate with comma (`,`). For the glob corresponds to multiple parsers, concatenate them with `&` mark. Each parser name needs to match the parsers shipped under `depcheck.parser`. 62 | 63 | The above CLI argument is equivalent to the following API options: 64 | 65 | ```js 66 | var opts = { 67 | parsers: { 68 | '*.js': depcheck.parser.es6, 69 | '*.jsx': depcheck.parser.jsx, 70 | '*.json': [ 71 | depcheck.parser.json1, 72 | depcheck.parser.json2, 73 | ] 74 | } 75 | }; 76 | ``` 77 | 78 | ### Implement Custom Parser 79 | 80 | Because the parser is just a normal JavaScript function. Everybody can implement its own syntax parser, then pass its own parser via API. 81 | 82 | The interface of a parser looks like: 83 | 84 | ```js 85 | function customerParser(content) { 86 | return ast || ['package', 'name', 'array']; 87 | } 88 | ``` 89 | 90 | There are two return value type can be handled by depcheck. The first type is AST. When implement your own AST, please follow [ESTree Spec](https://github.com/estree/estree), and provides `type` properties in your [node objects](https://github.com/estree/estree/blob/master/spec.md#node-objects). 91 | 92 | The second option is plain string array. The string array indicates these packages **is used** by the file. Depcheck will mark these packages as dependencies and **skip the detector step**. 93 | 94 | On the parse error case, throw `SyntaxError` exception and depcheck will capture it and store it to the `invalidFiles` property in the result. When multiple parse error happens, *only one* error is stored in the `invalidFiles` property. 95 | 96 | ## Detector 97 | 98 | After the file content is converted into an AST, the detectors are responsible to walk on each AST nodes to report dependency packages. 99 | 100 | Depcheck ships these detectors: 101 | 102 | - `requireCallExpreesion` detector for `require` function 103 | - `importDeclaration` detector for ES6 `import` declaration 104 | - `gruntLoadTaskCallExpression` detector for `grunt.tasks.loadNpmTasks` function 105 | - `expressViewEngine` detector for Express [view engine](https://expressjs.com/en/guide/using-template-engines.html) 106 | 107 | ### Use Detector From API 108 | 109 | Depcheck API exposes the `detectors` property in options to specify detectors. 110 | 111 | Here is the default `detectors` option value: 112 | 113 | ```js 114 | const opts = { 115 | detectors: [ 116 | depcheck.detector.requireCallExpression, 117 | depcheck.detector.importDeclaration, 118 | depcheck.detector.gruntLoadTaskCallExpression // for backward compatible 119 | ] 120 | }; 121 | ``` 122 | 123 | The `detectors` option accepts an array of detectors. **All** successful converted ASTs will be examined by **all** detectors one by one. 124 | 125 | ### Use Detector From CLI 126 | 127 | Depcheck CLI provdes `--detectors` argument to specify out-of-box detectors. The syntax looks like: 128 | 129 | ``` 130 | --detectors=requireCallExpression,anotherDetector 131 | ``` 132 | 133 | Each detector is concatenated with comma mark (`,`). 134 | 135 | The above CLI argument is equivalent to the following API options: 136 | 137 | ```js 138 | var opts = { 139 | detectors: [ 140 | depcheck.detector.requireCallExpression, 141 | depcheck.detector.anotherDetector 142 | ] 143 | }; 144 | ``` 145 | 146 | ### Implement Custom Detector 147 | 148 | Detector is a JavaScript function accepts an AST node and package dependencies (including devDependencies unless `withoutDev` option is `true`) and returns an array of dependency package names. 149 | 150 | The following code snippet is the ES6 `import` declaration detector: 151 | 152 | ```js 153 | function importDeclarationDetector(node, deps) { 154 | return node.type === 'ImportDeclaration' && node.source && node.source.value 155 | ? [node.source.value] 156 | : []; 157 | } 158 | ``` 159 | 160 | The returning array provides a chance to detect multiple dependencies from one node. The might be useful when handle Webpack or Babel configuration file. 161 | 162 | The package dependencies is passed into the detector. It provides the ability to figure out undetermined dependencies. For example, [webpack loader](http://webpack.github.io/docs/using-loaders.html#configuration) has a naming convention to strip out the `-loader` from the package name. So, from the source code aspect (the node object), it cannot figure out the dependency names. However, diff the candidates with the package dependencies, we are getting the answer. 163 | 164 | Please ensure your detector test node type before evaluate it - AST's `node.type` property is a good entry for your detector. Besides, do **not** throw exceptions from detector, the exception will be ignored and treat detector is returning an empty array. 165 | 166 | ## Special Parser 167 | 168 | Special parser is one kind of parser, but it is *special*. 169 | 170 | Usually, we find the using dependencies from source codes. But, sometimes, it is easier to target a specified dependency, then find whether it is used in the codes or not. That is the situation our *special* parser comes in. 171 | 172 | **Every** file will be passed to **every** special parser for evaluation. The special parser reports the dependency packages from files. 173 | 174 | ### Use Special Parser From API 175 | 176 | Depcheck API exposes `specials` property, which accepts an array, in options to specify special parsers. The syntax look like: 177 | 178 | ```js 179 | var opts = { 180 | specials: [ 181 | depcheck.special.eslint, 182 | depcheck.special.webpack 183 | ] 184 | }; 185 | ``` 186 | 187 | ### Use Special Parser From CLI 188 | 189 | Depcheck CLI exposes `--specials` argument to specify out-of-box special parsers. The syntax looks like: 190 | 191 | ``` 192 | --specials=bin,eslint 193 | ``` 194 | 195 | The above example is equivalent to the following API options: 196 | 197 | ```js 198 | var opts = { 199 | specials: [ 200 | depcheck.special.bin, 201 | depcheck.special.eslint 202 | ] 203 | }; 204 | ``` 205 | 206 | ### Implement Custom Special Parser 207 | 208 | Special parser is just one kind of parsers. It has the same interface with parser, with more arguments. The following code snippet is a special parser for [eslint-config-airbnb](https://www.npmjs.com/package/eslint-config-airbnb) package: 209 | 210 | ```js 211 | function airbnbEslintConfig(content, filePath, deps, dir) { 212 | var filename = path.basename(filePath); 213 | if (filename === '.eslintrc' && deps.indexOf('eslint-config-airbnb') !== -1) { 214 | var eslintConfig = JSON.parser(content); 215 | if (eslintConfig.extends === 'airbnb') { 216 | return ['eslint-config-airbnb', 'eslint', 'babel-eslint', 'eslint-plugin-react']; 217 | } 218 | } 219 | 220 | return []; 221 | } 222 | ``` 223 | 224 | As seen from the code snippet, there are four parameters passed into the *special* parser: 225 | 226 | - Content, same as normal parser, the file content. 227 | - FilePath, the file path, use `path.basename` to retrieve the file name. 228 | - Deps, an array containing the package dependencies (including devDependencies unless `withoutDev` option is `true`). 229 | - Dir, the checking root directory passed from API or CLI. 230 | 231 | Pay attention that, special parser will match **all** files, please do file path or file name matching **by yourself** and only parse content only when necessary. In regards to the returning value, both AST node or plain string array are OK as a normal parser. 232 | 233 | --- 234 | 235 | If you have any ideas or suggestions, please [open an issue](https://github.com/depcheck/depcheck/issues/new). 236 | -------------------------------------------------------------------------------- /test/spec.js: -------------------------------------------------------------------------------- 1 | import depcheck from '../src/index'; 2 | 3 | export default [ 4 | { 5 | name: 'missing module for require.resolve when missing in package.json', 6 | module: 'require_resolve_missing', 7 | options: { 8 | withoutDev: true, 9 | }, 10 | expected: { 11 | dependencies: [], 12 | devDependencies: [], 13 | missing: { 14 | anyone: ['index.js'], 15 | }, 16 | using: { 17 | anyone: ['index.js'], 18 | }, 19 | }, 20 | }, 21 | { 22 | name: 'find module for require.resolve when present', 23 | module: 'require_resolve', 24 | options: { 25 | withoutDev: true, 26 | }, 27 | expected: { 28 | dependencies: [], 29 | devDependencies: [], 30 | missing: {}, 31 | using: { 32 | optimist: ['index.js'], 33 | }, 34 | }, 35 | }, 36 | { 37 | name: 'find unused dependencies', 38 | module: 'bad', 39 | options: { 40 | withoutDev: true, 41 | }, 42 | expected: { 43 | dependencies: ['optimist'], 44 | devDependencies: [], 45 | missing: {}, 46 | using: {}, 47 | }, 48 | }, 49 | { 50 | name: 'find unused dependencies in ES6 files', 51 | module: 'bad_es6', 52 | options: { 53 | }, 54 | expected: { 55 | dependencies: ['dont-find-me'], 56 | devDependencies: [], 57 | missing: {}, 58 | using: { 59 | 'find-me': ['index.js'], 60 | 'default-member-import': ['index.js'], 61 | 'member-alias-import': ['index.js'], 62 | 'member-import': ['index.js'], 63 | 'mixed-default-star-import': ['index.js'], 64 | 'mixed-member-alias-import': ['index.js'], 65 | 'mixed-name-memeber-import': ['index.js'], 66 | 'multiple-member-import': ['index.js'], 67 | 'name-import': ['index.js'], 68 | 'star-import': ['index.js'], 69 | }, 70 | }, 71 | }, 72 | { 73 | name: 'find all dependencies', 74 | module: 'good', 75 | options: { 76 | withoutDev: true, 77 | }, 78 | expected: { 79 | dependencies: [], 80 | devDependencies: [], 81 | missing: {}, 82 | using: { 83 | optimist: ['index.js'], 84 | }, 85 | }, 86 | }, 87 | { 88 | // See `good_es6/index.js` file for more information about the unsupported 89 | // ES6 import syntax, which we assert here as the expected missing import. 90 | name: 'find all dependencies in ES6 files', 91 | module: 'good_es6', 92 | options: { 93 | withoutDev: true, 94 | }, 95 | expected: { 96 | dependencies: ['unsupported-syntax'], 97 | devDependencies: [], 98 | missing: {}, 99 | using: { 100 | 'basic-import': ['index.js'], 101 | 'default-member-import': ['index.js'], 102 | 'member-alias-import': ['index.js'], 103 | 'member-import': ['index.js'], 104 | 'mixed-default-star-import': ['index.js'], 105 | 'mixed-member-alias-import': ['index.js'], 106 | 'mixed-name-memeber-import': ['index.js'], 107 | 'multiple-member-import': ['index.js'], 108 | 'name-import': ['index.js'], 109 | 'star-import': ['index.js'], 110 | }, 111 | }, 112 | }, 113 | { 114 | name: 'recognize experimental ES7 syntax enabled in Babel by default', 115 | module: 'good_es7', 116 | options: { 117 | withoutDev: true, 118 | }, 119 | expected: { 120 | dependencies: [], 121 | devDependencies: [], 122 | missing: {}, 123 | using: { 124 | 'ecmascript-rest-spread': ['index.js'], 125 | }, 126 | }, 127 | }, 128 | { 129 | name: 'support Typescript syntax', 130 | module: 'typescript', 131 | options: { 132 | }, 133 | expected: { 134 | dependencies: ['unused-dep'], 135 | devDependencies: [], 136 | missing: {}, 137 | using: { 138 | react: ['component.tsx'], 139 | 'ts-dep-1': ['index.ts'], 140 | 'ts-dep-2': ['index.ts'], 141 | }, 142 | }, 143 | }, 144 | { 145 | name: 'support SASS/SCSS syntax', 146 | module: 'sass', 147 | options: { 148 | }, 149 | expected: { 150 | dependencies: ['unused-sass-dep'], 151 | devDependencies: [], 152 | missing: {}, 153 | using: { 154 | 'sass-dep': ['sass.sass'], 155 | 'scss-dep': ['scss.scss'], 156 | }, 157 | }, 158 | }, 159 | { 160 | name: 'find dependencies used in code but not declared in package.json', 161 | module: 'missing', 162 | options: { 163 | }, 164 | expected: { 165 | dependencies: [], 166 | devDependencies: [], 167 | missing: { 168 | 'missing-dep': ['index.js'], 169 | }, 170 | using: { 171 | 'missing-dep': ['index.js'], 172 | }, 173 | }, 174 | }, 175 | { 176 | name: 'ignore the missing dependencies in nested module', 177 | module: 'missing_nested', 178 | options: { 179 | }, 180 | expected: { 181 | dependencies: [], 182 | devDependencies: [], 183 | missing: { 184 | 'outer-missing-dep': ['index.js'], 185 | }, 186 | using: { 187 | 'outer-missing-dep': ['index.js'], 188 | 'used-dep': ['index.js'], 189 | }, 190 | }, 191 | }, 192 | { 193 | name: 'not report peer and optional dependencies as missing', 194 | module: 'missing_peer_deps', 195 | options: { 196 | }, 197 | expected: { 198 | dependencies: [], 199 | devDependencies: [], 200 | missing: { 201 | 'missing-this-dep': ['index.js'], 202 | }, 203 | using: { 204 | 'missing-this-dep': ['index.js'], 205 | 'peer-dep': ['index.js'], 206 | 'optional-dep': ['index.js'], 207 | }, 208 | }, 209 | }, 210 | { 211 | name: 'find grunt dependencies', 212 | module: 'grunt', 213 | options: { 214 | withoutDev: true, 215 | }, 216 | expected: { 217 | dependencies: [], 218 | devDependencies: [], 219 | missing: {}, 220 | using: { 221 | 'grunt-contrib-jshint': ['index.js'], 222 | }, 223 | }, 224 | }, 225 | { 226 | name: 'find grunt task dependencies', 227 | module: 'grunt-tasks', 228 | options: { 229 | withoutDev: true, 230 | }, 231 | expected: { 232 | dependencies: [], 233 | devDependencies: [], 234 | missing: {}, 235 | using: { 236 | 'grunt-contrib-jshint': ['index.js'], 237 | }, 238 | }, 239 | }, 240 | { 241 | name: 'find unused package in devDependencies', 242 | module: 'dev', 243 | options: { 244 | withoutDev: false, 245 | }, 246 | expected: { 247 | dependencies: [], 248 | devDependencies: ['unused-dev-dep'], 249 | missing: {}, 250 | using: { 251 | 'used-dep': ['index.js'], 252 | }, 253 | }, 254 | }, 255 | { 256 | name: 'recognize peer dependencies', 257 | module: 'peer_dep', 258 | options: { 259 | }, 260 | expected: { 261 | dependencies: ['unused-dep'], 262 | devDependencies: [], 263 | missing: {}, 264 | using: { 265 | host: ['index.js'], 266 | peer: ['index.js'], 267 | }, 268 | }, 269 | }, 270 | { 271 | name: 'recognize nested peer dependencies', 272 | module: 'peer_dep_nested', 273 | options: { 274 | }, 275 | expected: { 276 | dependencies: ['unused-nested-dep'], 277 | devDependencies: [], 278 | missing: {}, 279 | using: { 280 | host: ['nested/index.js'], 281 | peer: ['nested/index.js'], 282 | }, 283 | }, 284 | }, 285 | { 286 | name: 'recognize optional dependencies', 287 | module: 'optional_dep', 288 | options: { 289 | }, 290 | expected: { 291 | dependencies: ['unused-dep'], 292 | devDependencies: [], 293 | missing: {}, 294 | using: { 295 | host: ['index.js'], 296 | optional: ['index.js'], 297 | }, 298 | }, 299 | }, 300 | { 301 | name: 'recognize nested requires', 302 | module: 'nested', 303 | options: { 304 | }, 305 | expected: { 306 | dependencies: [], 307 | devDependencies: [], 308 | missing: {}, 309 | using: { 310 | optimist: ['index.js'], 311 | }, 312 | }, 313 | }, 314 | { 315 | name: 'handle empty JavaScript file', 316 | module: 'empty_file', 317 | options: { 318 | }, 319 | expected: { 320 | dependencies: ['empty-package'], 321 | devDependencies: [], 322 | missing: {}, 323 | using: {}, 324 | }, 325 | }, 326 | { 327 | name: 'handle script file with node shebang', 328 | module: 'shebang', 329 | options: { 330 | }, 331 | expected: { 332 | dependencies: ['shebang'], 333 | devDependencies: [], 334 | missing: {}, 335 | using: { 336 | 'shebang-script': ['index.js'], 337 | }, 338 | }, 339 | }, 340 | { 341 | name: 'handle a package without any dependencies', 342 | module: 'empty_dep', 343 | options: { 344 | }, 345 | expected: { 346 | dependencies: [], 347 | devDependencies: [], 348 | missing: {}, 349 | using: {}, 350 | }, 351 | }, 352 | { 353 | name: 'exclude bin dependencies if ignoreBinPackage equal true', 354 | module: 'bin_js', 355 | options: { 356 | ignoreBinPackage: true, 357 | }, 358 | expected: { 359 | dependencies: [], 360 | devDependencies: [], 361 | missing: {}, 362 | using: {}, 363 | }, 364 | }, 365 | { 366 | name: 'report unused bin dependencies if ignoreBinPackage equal false', 367 | module: 'bin_js', 368 | options: { 369 | ignoreBinPackage: false, 370 | }, 371 | expected: { 372 | dependencies: ['anybin'], 373 | devDependencies: [], 374 | missing: {}, 375 | using: {}, 376 | }, 377 | }, 378 | { 379 | name: 'handle dependencies without bin if ignoreBinPackage equal true', 380 | module: 'good', 381 | options: { 382 | ignoreBinPackage: true, 383 | }, 384 | expected: { 385 | dependencies: [], 386 | devDependencies: [], 387 | missing: {}, 388 | using: { 389 | optimist: ['index.js'], 390 | }, 391 | }, 392 | }, 393 | { 394 | name: 'not ignore bin dependencies when ignoreBinPackage is false', 395 | module: 'bin_js', 396 | options: { 397 | ignoreBinPackage: false, 398 | }, 399 | expected: { 400 | dependencies: ['anybin'], 401 | devDependencies: [], 402 | missing: {}, 403 | using: {}, 404 | }, 405 | }, 406 | { 407 | name: 'output empty missing dependencies when skipMissing is true', 408 | module: 'missing', 409 | options: { 410 | skipMissing: true, 411 | }, 412 | expected: { 413 | dependencies: [], 414 | devDependencies: [], 415 | missing: {}, 416 | using: { 417 | 'missing-dep': ['index.js'], 418 | }, 419 | }, 420 | }, 421 | { 422 | name: 'output missing dependencies when skipMissing is false', 423 | module: 'missing', 424 | options: { 425 | skipMissing: false, 426 | }, 427 | expected: { 428 | dependencies: [], 429 | devDependencies: [], 430 | missing: { 431 | 'missing-dep': ['index.js'], 432 | }, 433 | using: { 434 | 'missing-dep': ['index.js'], 435 | }, 436 | }, 437 | }, 438 | { 439 | name: 'handle require call without parameters', 440 | module: 'require_nothing', 441 | options: { 442 | }, 443 | expected: { 444 | dependencies: ['require-nothing'], 445 | devDependencies: [], 446 | missing: {}, 447 | using: {}, 448 | }, 449 | }, 450 | { 451 | name: 'handle require call with dynamic expression', 452 | module: 'require_dynamic', 453 | options: { 454 | }, 455 | expected: { 456 | dependencies: [], 457 | devDependencies: [], 458 | missing: {}, 459 | using: { 460 | dynamic: ['index.js'], 461 | }, 462 | }, 463 | }, 464 | { 465 | name: 'ignore ignoreDirs', 466 | module: 'bad_deep', 467 | options: { 468 | ignoreDirs: ['sandbox'], 469 | }, 470 | expected: { 471 | dependencies: ['module_bad_deep'], 472 | devDependencies: [], 473 | missing: {}, 474 | using: {}, 475 | }, 476 | }, 477 | { 478 | name: 'ignore ignoreMatches', 479 | module: 'bad', 480 | options: { 481 | ignoreMatches: ['o*'], 482 | }, 483 | expected: { 484 | dependencies: [], 485 | devDependencies: [], 486 | missing: {}, 487 | using: {}, 488 | }, 489 | }, 490 | { 491 | name: 'ignore ignoreMatches for missing', 492 | module: 'missing_ignore', 493 | options: { 494 | ignoreMatches: ['missing-ignore-*'], 495 | }, 496 | expected: { 497 | dependencies: [], 498 | devDependencies: [], 499 | missing: { 500 | 'missing-dep': ['index.js'], 501 | }, 502 | using: { 503 | 'missing-dep': ['index.js'], 504 | 'missing-ignore-dep': ['index.js'], 505 | }, 506 | }, 507 | }, 508 | { 509 | name: 'support jsx syntax', 510 | module: 'jsx', 511 | options: { 512 | }, 513 | expected: { 514 | dependencies: [], 515 | devDependencies: [], 516 | missing: {}, 517 | using: { 518 | react: ['index.jsx'], 519 | }, 520 | }, 521 | }, 522 | { 523 | name: 'parser jsx syntax in JavaScript file by default', 524 | module: 'jsx_js', 525 | options: { 526 | }, 527 | expected: { 528 | dependencies: [], 529 | devDependencies: [], 530 | missing: {}, 531 | using: { 532 | react: ['index.js'], 533 | 'jsx-as-js': ['index.js'], 534 | }, 535 | }, 536 | }, 537 | { 538 | name: 'support CoffeeScript syntax', 539 | module: 'coffee_script', 540 | options: { 541 | }, 542 | expected: { 543 | dependencies: ['coffee'], 544 | devDependencies: [], 545 | missing: {}, 546 | using: { 547 | bar: ['index.coffee'], 548 | baz: ['index.coffee'], 549 | foo: ['index.coffee'], 550 | }, 551 | }, 552 | }, 553 | { 554 | name: 'support scoped modules', 555 | module: 'scoped_module', 556 | options: { 557 | }, 558 | expected: { 559 | dependencies: ['@unused/package'], 560 | devDependencies: [], 561 | missing: {}, 562 | using: { 563 | '@owner/package': ['index.js'], 564 | '@secondowner/package': ['index.js'], 565 | '@org/parent': ['index.js'], 566 | 'name-import': ['index.js'], 567 | 'child-import': ['index.js'], 568 | }, 569 | }, 570 | }, 571 | { 572 | name: 'ignore require number', 573 | module: 'ignore_number', 574 | options: { 575 | }, 576 | expected: { 577 | dependencies: ['number'], 578 | devDependencies: [], 579 | missing: {}, 580 | using: {}, 581 | }, 582 | }, 583 | { 584 | name: 'discover dependencies from mocha opts specified by scripts', 585 | module: 'mocha_opts', 586 | options: { 587 | }, 588 | expected: { 589 | dependencies: [], 590 | devDependencies: ['mocha'], 591 | missing: {}, 592 | using: { 593 | babel: ['package.json'], 594 | chai: ['package.json'], 595 | }, 596 | }, 597 | }, 598 | { 599 | name: 'discover dependency from express view engine setting', 600 | module: 'express_view_engine', 601 | options: { 602 | detectors: [ 603 | depcheck.detector.requireCallExpression, 604 | depcheck.detector.expressViewEngine, 605 | ], 606 | }, 607 | expected: { 608 | dependencies: [], 609 | devDependencies: [], 610 | missing: {}, 611 | using: { 612 | ejs: ['index.js'], 613 | express: ['index.js'], 614 | }, 615 | }, 616 | }, 617 | ]; 618 | --------------------------------------------------------------------------------