├── .eslintrc.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── api.js ├── appveyor.yml ├── cli.js ├── fixtures ├── component.js ├── default-anonymous.js ├── es6.js └── parsing-error.js ├── lib ├── ast-helpers.js ├── common.js ├── dep-registry.js ├── importer.js ├── parser.js └── walker.js ├── logo.png ├── package.json ├── patches ├── escope.js └── progress.js ├── test ├── api.js ├── cli.js ├── importer.js ├── importer.yml ├── parser.js └── parser.yml ├── visits ├── find-exports.js ├── find-imports.js ├── find-style.js └── resolve-jsx.js └── yarn.lock /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | ecmaVersion: 6 3 | sourceType: module 4 | ecmaFeatures: 5 | jsx: true 6 | env: 7 | node: true 8 | es6: true 9 | root: true 10 | extends: 11 | - eslint:recommended 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | tmp/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | tmp/ 4 | fixtures/ 5 | test/ 6 | yarn.lock 7 | logo.png 8 | appveyor.yml 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: [6, 7] 3 | sudo: false 4 | cache: 5 | directories: [node_modules] 6 | script: npm test 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017 Karthik Viswanathan 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 | 24 | This software includes some code from ESLint. ESLint is under the following 25 | license: 26 | 27 | ESLint 28 | Copyright JS Foundation and other contributors, https://js.foundation 29 | 30 | Permission is hereby granted, free of charge, to any person obtaining a copy of 31 | this software and associated documentation files (the "Software"), to deal in 32 | the Software without restriction, including without limitation the rights to 33 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 34 | the Software, and to permit persons to whom the Software is furnished to do so, 35 | subject to the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be included in 38 | all copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 43 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 44 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 45 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 46 | THE SOFTWARE. 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![tradeship][logo-image]][tradeship-url] 2 | 3 | [![Linux Build][travis-image]][travis-url] 4 | [![Windows Build][appveyor-image]][appveyor-url] 5 | [![NPM Version][npm-image]][npm-url] 6 | 7 | tradeship statically analyzes your JavaScript code for identifiers that aren't 8 | defined and finds the appropriate dependencies to import. It also removes 9 | imports that aren't used. tradeship is meant to be used as an editor plugin that 10 | manages your imports. 11 | 12 |  13 | 14 | ## Features 15 | - Imports dependencies from node.js standard libraries, npm packages listed in 16 | package.json, and other files within your project directory. 17 | - Infers properties that are exported by dependencies and imports them using 18 | destructuring syntax (e.g. `const { Component } = require("react");`). 19 | - Knows aliases for dependencies (e.g. `$` for jQuery, `_` for 20 | lodash/underscore, `test` for ava, etc.) by analyzing more than 100 GB of 21 | public JS code on GitHub. 22 | - Automatically identifies the style of your code and makes the imports match 23 | (e.g. single vs. double quotes, semicolons vs. no semicolons, var vs. let vs. 24 | const, etc.) 25 | - Statically analyzes files within your project directory to determine their 26 | exports for use in other files. 27 | - Supports both CommonJS and ES6 modules. Can output `require()` or `import` 28 | syntax. 29 | - Supports JSX identifiers and ensures React is in scope when using JSX. 30 | 31 | ## Installation 32 | Install tradeship using npm or yarn. It's recommended to either install 33 | tradeship globally or make the local installation available on your path (e.g. 34 | by adding `export PATH=node_modules/.bin:$PATH` to your shell configuration), 35 | since editor plugins make use of the `tradeship` executable. 36 | 37 | ```sh 38 | $ npm install -g tradeship 39 | # or use yarn: 40 | $ yarn global add tradeship 41 | ``` 42 | 43 | Then, install the editor plugin of your choice: 44 | 45 | - Vim: [`tradeship-vim`](https://github.com/karthikv/tradeship-vim) 46 | - Atom: [`tradeship-atom`](https://github.com/karthikv/tradeship-atom) 47 | - Sublime: [`tradeship-sublime`](https://github.com/karthikv/tradeship-sublime) 48 | - Emacs: [`tradeship-emacs`](https://github.com/karthikv/tradeship-emacs) 49 | - VS Code: [vscode-tradeship](https://marketplace.visualstudio.com/items?itemName=taichi.vscode-tradeship) 50 | 51 | Each editor plugin has instructions on how to run tradeship on the current file 52 | being edited. You can also configure each plugin to run tradeship on save. See 53 | the respective links above for more information. 54 | 55 | The first time tradeship runs in a project directory with many JavaScript files, 56 | it'll take some time to parse and cache dependencies. Future runs will be much 57 | faster. If you'd like to use tradeship in a large project, it's recommended to 58 | run it once on the command-line, as shown below, so you can see the progress as 59 | it populates the cache: 60 | 61 | ```sh 62 | tradeship [path] 63 | ``` 64 | 65 | Replace `[path]` with any path to a JavaScript file in your project directory. 66 | Note that this won't modify the file in any way; it'll just print the new code 67 | to stdout. See the [Command Line Interface](#command-line-interface-cli) section 68 | for details. 69 | 70 | ## Configuration 71 | tradeship doesn't need any configuration to work out of the box. By design, 72 | almost nothing is configurable, as tradeship automatically infers style from 73 | your code. There is, however, one configuration option that you may want to 74 | tweak: globals. 75 | 76 | To correctly find identifiers that aren't defined, tradeship needs to know what 77 | global variables are available. It does this primarily through environments, 78 | where each environment defines a set of global variables that come with it. 79 | tradeship piggybacks on [eslint's configuration 80 | system](http://eslint.org/docs/user-guide/configuring) to avoid introducing 81 | another configuration file and format. 82 | 83 | In eslint, you specify the environments you want in your configuration file, 84 | generally at the root of your project directory. tradeship searches for an 85 | eslint configuration file (either `.eslintrc.js`, `.eslintrc.yaml`, 86 | `.eslintrc.yml`, `.eslintrc.json`, `.eslintrc`, or an `eslintConfig` object in 87 | `package.json`) within the code's directory or successive parent directories. 88 | 89 | When it finds one, it looks at the `env` object to determine the active 90 | environments. An example configuration object is: 91 | 92 | ```js 93 | { 94 | "env": { 95 | "browser": true, 96 | "es6": true 97 | } 98 | } 99 | ``` 100 | 101 | Each key is an environment name, and the corresponding value is whether that 102 | environment is active. See eslint's guide to [specifying 103 | environments](http://eslint.org/docs/user-guide/configuring#specifying-environments) 104 | for more details about the available environments. 105 | 106 | If there's no configuration file, `tradeship` assumes the environments 107 | `browser`, `node`, and `es6`, bringing in globals from the browser (e.g. 108 | `window` and `document`), from node.js (e.g. `process` and `__dirname`), and 109 | from ES6 (e.g. `Set` and `Map`). 110 | 111 | Note that tradeship makes all the node.js standard libraries available for 112 | import if and only if the `node` environment is active. 113 | 114 | In addition to environment-based globals, you can also specify any globals 115 | specific to your project or build process using the `globals` object. Each key 116 | is the name of a global, and the value is whether that global is writable 117 | (unused by tradeship, but used by eslint). For instance, the following registers 118 | `React` and `$` as globals, so tradeship won't attempt to import them: 119 | 120 | ```js 121 | { 122 | "globals": { 123 | "React": false, 124 | "$": false 125 | } 126 | } 127 | ``` 128 | 129 | ## Command Line Interface (CLI) 130 | Using an editor plugin (see section above) is the easiest way to get started 131 | with tradeship, but you can also use the command line interface (which all 132 | editor plugins use internally). 133 | 134 | ```sh 135 | tradeship [options] [path] 136 | ``` 137 | 138 | Reads the code given at `[path]`. Outputs new code to stdout, adding missing 139 | dependencies and removing unused ones. The `[options]` are as follows: 140 | 141 | - `-s` or `--stdin`: Read contents from stdin as opposed to `[path]`. `[path]` 142 | is still required so that tradeship can resolve relative imports and find 143 | available npm packages, but it need not exist as a file; you can even just 144 | provide a directory. 145 | 146 | - `-w` or `--write`: Write output back to `[path]` (be careful!). 147 | 148 | - `-h` or `--help`: Print help. 149 | 150 | - `-v` or `--version`: Print version. 151 | 152 | The full help text is below: 153 | 154 | ``` 155 | Usage: tradeship [options] [path] 156 | Automatically imports missing JS dependencies and removes unused ones. 157 | 158 | Options: 159 | -s, --stdin read contents from stdin 160 | -w, --write write output to source file (careful!) 161 | -h, --help print help 162 | -v, --version print version 163 | 164 | Arguments: 165 | [path] Relative imports and available npm packages will be determined 166 | from this path. If not --stdin, input code will be read from this 167 | path. If --write, new code will be written to this path. 168 | ``` 169 | 170 | ## Node.js Interface 171 | tradeship exposes a simple node.js API if you'd like to use it programmatically: 172 | 173 | ```js 174 | const tradeship = require("tradeship"); 175 | tradeship.import(dir, code).then(newCode => { 176 | // do something with newCode 177 | }); 178 | ``` 179 | 180 | `dir` is the directory used to resolve relative imports and find available npm 181 | packages (generally the directory where the `code` comes from). `code` is the 182 | actual JavaScript code. `tradeship.import()` returns a promise that, when 183 | resolved, gives the resulting new code. 184 | 185 | ## How it works 186 | `tradeship` analyzes dependencies from three sources: node.js standard 187 | libraries, package.json dependencies, and other files within your project 188 | directory. 189 | 190 | For each dependency it finds, tradeship: 191 | 192 | - **Determines potential import names**: 193 | 194 | An import name is a variable name you might see in code that refers to the 195 | dependency. For instance, the import name `fs` would refer to the `fs` node.js 196 | standard library. The import name `React` would refer to the `react` npm 197 | package. 198 | 199 | For node.js standard libraries and package.json packages, if the 200 | library/package name is itself a valid JavaScript identifier, it and its 201 | capitalized version are potential import names (e.g. `react` and `React` for 202 | the `react` npm package). If the library/package name has non-word characters 203 | or underscores, it is split on `[\W_]+` and the parts are joined, both in 204 | camel and class case, to get two more import names (`childProcess` and 205 | `ChildProcess` for the `child_process` node.js standard library). 206 | 207 | There are various package.json packages that are imported under common aliases 208 | known by the community (e.g. `$` for jQuery, `_` for lodash/underscore, `test` 209 | for ava, etc.). By analyzing more than 100 GB of public JS code on GitHub, 210 | tradeship knows many such aliases, and will import the associated package. 211 | 212 | For project files, the code is statically analyzed to find an import name. For 213 | instance, if you write `module.exports = SomeExport;`, `SomeExport` will be an 214 | import name. This is one simple case; there are many others that tradeship 215 | parses, including those with ES6 `export` syntax. In addition to the analyzed 216 | import name, tradeship also uses the file path's base name as an import name, 217 | using the same logic as defined for node.js standard library names above. 218 | 219 | - **Determines properties**: 220 | 221 | These are JavaScript object properties that are exported by the dependency. 222 | For instance, `readFile` is a property of the `fs` node.js standard library. 223 | `Component` is a property of the `react` npm package. The import name of 224 | properties is equivalent to the property name. 225 | 226 | For node.js standard libraries and package.json packages, the library is 227 | loaded within a separate node.js process, and properties are extracted using 228 | `Object.keys()`. 229 | 230 | For project files, the code is statically analyzed to find properties. For 231 | instance, if you write `exports.someProperty = ...;`, `someProperty` will be 232 | a parsed property. This is one simple case; there are many others that 233 | tradeship parses, including those with ES6 `export` syntax. 234 | 235 | Then, tradeship analyzes your code for identifiers that aren't defined. Each 236 | identifier is a potential import name, and tradeship searches for the 237 | corresponding dependency or property. If it finds a match, it adds the 238 | appropriate import to the code. If multiple dependencies or properties match 239 | a given import name, tradeship prioritizes them as follows: 240 | 241 | 1. Project file dependency (highest priority) 242 | 1. Project file property 243 | 1. package.json dependency 244 | 1. package.json property 245 | 1. Node.js standard library dependency 246 | 1. Node.js standard library property (lowest priority) 247 | 248 | tradeship groups all node.js and package.json dependencies together, sorted 249 | lexicographically. It then adds a blank line and all project file dependencies, 250 | also sorted lexicographically. 251 | 252 | tradeship finds all imports that aren't used and removes them. 253 | 254 | ## License 255 | [MIT](https://github.com/karthikv/tradeship/blob/master/LICENSE.md) 256 | 257 | ship vector icon by [Vecteezy](https://www.vecteezy.com/) 258 | 259 | [tradeship-url]: https://github.com/karthikv/tradeship 260 | [logo-image]: https://raw.githubusercontent.com/karthikv/tradeship/master/logo.png 261 | [travis-image]: https://img.shields.io/travis/karthikv/tradeship/master.svg?label=linux 262 | [travis-url]: https://travis-ci.org/karthikv/tradeship 263 | [appveyor-image]: https://img.shields.io/appveyor/ci/karthikv/tradeship/master.svg?label=windows 264 | [appveyor-url]: https://ci.appveyor.com/project/karthikv/tradeship 265 | [npm-image]: https://img.shields.io/npm/v/tradeship.svg 266 | [npm-url]: https://npmjs.org/package/tradeship 267 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const importer = require("./lib/importer"); 4 | 5 | exports.import = function(dir, code) { 6 | return importer.run(dir, code); 7 | }; 8 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "6" 4 | - nodejs_version: "7" 5 | cache: [node_modules] 6 | install: 7 | - ps: Install-Product node $env:nodejs_version 8 | - npm install 9 | test_script: 10 | - node --version 11 | - npm --version 12 | - npm test 13 | build: off 14 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint no-console: ["error", { allow: ["error"] }] */ 3 | "use strict"; 4 | 5 | const path = require("path"); 6 | 7 | const { readFile, stat, writeFile } = require("./lib/common"); 8 | const importer = require("./lib/importer"); 9 | 10 | const args = process.argv.slice(2); 11 | const options = { 12 | stdin: false, 13 | write: false, 14 | help: false, 15 | version: false, 16 | path: null 17 | }; 18 | 19 | args.forEach(arg => { 20 | if (arg === "-s" || arg === "--stdin") { 21 | options.stdin = true; 22 | } else if (arg === "-w" || arg === "--write") { 23 | options.write = true; 24 | } else if (arg === "-h" || arg === "--help") { 25 | options.help = true; 26 | } else if (arg === "-v" || arg === "--version") { 27 | options.version = true; 28 | } else if (arg[0] !== "-") { 29 | if (options.path) { 30 | console.error("Can only specify a single path"); 31 | process.exit(1); 32 | } 33 | options.path = arg; 34 | } else { 35 | console.error("Unexpected option", arg); 36 | process.exit(1); 37 | } 38 | }); 39 | 40 | if (options.help || args.length === 0) { 41 | const lines = [ 42 | "Usage: tradeship [options] [path]", 43 | "Automatically imports missing JS dependencies and removes unused ones.", 44 | "", 45 | "Options:", 46 | "-s, --stdin read contents from stdin", 47 | "-w, --write write output to source file (careful!)", 48 | "-h, --help print help", 49 | "-v, --version print version", 50 | "", 51 | "Arguments:", 52 | "[path] Relative imports and available npm packages will be determined", 53 | " from this path. If not --stdin, input code will be read from this", 54 | " path. If --write, new code will be written to this path." 55 | ]; 56 | console.error(lines.join("\n")); 57 | process.exit(0); 58 | } 59 | 60 | if (options.version) { 61 | const pkg = require("./package.json"); 62 | console.error(pkg.version); 63 | process.exit(0); 64 | } 65 | 66 | if (!options.path) { 67 | let message = "Must specify a path "; 68 | if (options.stdin) { 69 | message += "to resolve relative imports and find available npm packages."; 70 | } else { 71 | message += "that contains the input source code."; 72 | } 73 | console.error(message); 74 | process.exit(1); 75 | } 76 | 77 | let codePromise; 78 | if (options.stdin) { 79 | codePromise = new Promise((resolve, reject) => { 80 | let contents = ""; 81 | process.stdin.on("data", chunk => contents += chunk); 82 | process.stdin.on("end", () => resolve(contents)); 83 | process.stdin.on("error", reject); 84 | }); 85 | } else { 86 | codePromise = readFile(options.path, "utf8"); 87 | } 88 | 89 | let code; 90 | codePromise 91 | .then(c => { 92 | code = c; 93 | if (options.stdin) { 94 | return stat(options.path); 95 | } 96 | return null; 97 | }) 98 | .catch(err => { 99 | if (err.code === "ENOENT" && options.write) { 100 | return null; 101 | } 102 | throw err; 103 | }) 104 | .then(s => { 105 | if (s && s.isDirectory()) { 106 | return importer.run(options.path, code); 107 | } else { 108 | return importer.run(path.dirname(options.path), code); 109 | } 110 | }) 111 | .then(newCode => { 112 | if (options.write) { 113 | return writeFile(options.path, newCode); 114 | } else { 115 | process.stdout.write(newCode); 116 | } 117 | }) 118 | .catch(err => { 119 | if (err instanceof SyntaxError && err.loc) { 120 | const codeFrame = require("babel-code-frame"); 121 | const { line, column } = err.loc; 122 | 123 | const frame = codeFrame(code, line, column + 1, { highlightCode: true }); 124 | console.error(`${err.message}\n${frame}`); 125 | process.exit(1); 126 | } 127 | 128 | console.error(err); 129 | process.exit(1); 130 | }); 131 | -------------------------------------------------------------------------------- /fixtures/component.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | 3 | module.exports = class Component extends React.Component { 4 | render() { 5 | return
Paragraph
; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /fixtures/default-anonymous.js: -------------------------------------------------------------------------------- 1 | export default 3; 2 | -------------------------------------------------------------------------------- /fixtures/es6.js: -------------------------------------------------------------------------------- 1 | const es6 = {}; 2 | export default es6; 3 | export const prop = 3; 4 | -------------------------------------------------------------------------------- /fixtures/parsing-error.js: -------------------------------------------------------------------------------- 1 | class A extends {} 2 | -------------------------------------------------------------------------------- /lib/ast-helpers.js: -------------------------------------------------------------------------------- 1 | const flowNodes = new Set([ 2 | "DeclaredPredicate", 3 | "InferredPredicate", 4 | "DeclareClass", 5 | "FunctionTypeAnnotation", 6 | "TypeAnnotation", 7 | "DeclareFunction", 8 | "DeclareVariable", 9 | "DeclareModule", 10 | "DeclareModuleExports", 11 | "DeclareTypeAlias", 12 | "DeclareInterface", 13 | "InterfaceExtends", 14 | "InterfaceDeclaration", 15 | "TypeAlias", 16 | "TypeParameter", 17 | "TypeParameterDeclaration", 18 | "TypeParameterInstantiation", 19 | "ObjectTypeIndexer", 20 | "ObjectTypeProperty", 21 | "ObjectTypeCallProperty", 22 | "ObjectTypeAnnotation", 23 | "QualifiedTypeIdentifier", 24 | "GenericTypeAnnotation", 25 | "TypeofTypeAnnotation", 26 | "TupleTypeAnnotation", 27 | "FunctionTypeParam", 28 | "AnyTypeAnnotation", 29 | "VoidTypeAnnotation", 30 | "BooleanTypeAnnotation", 31 | "MixedTypeAnnotation", 32 | "EmptyTypeAnnotation", 33 | "NumberTypeAnnotation", 34 | "StringTypeAnnotation", 35 | "BooleanLiteralTypeAnnotation", 36 | "NullLiteralTypeAnnotation", 37 | "ThisTypeAnnotation", 38 | "ExistsTypeAnnotation", 39 | "ArrayTypeAnnotation", 40 | "NullableTypeAnnotation", 41 | "IntersectionTypeAnnotation", 42 | "UnionTypeAnnotation", 43 | "Variance", 44 | "TypeCastExpression", 45 | "ClassImplements" 46 | ]); 47 | 48 | exports.isFlowNode = function(node) { 49 | return flowNodes.has(node.type); 50 | }; 51 | 52 | exports.isFlowImport = function(node) { 53 | return node.importKind && node.importKind !== "value"; 54 | }; 55 | 56 | exports.isFlowExport = function(node) { 57 | return node.exportKind && node.exportKind !== "value"; 58 | }; 59 | -------------------------------------------------------------------------------- /lib/common.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const yaml = require("js-yaml"); 5 | const path = require("path"); 6 | 7 | exports.debug = require("debug")("tradeship"); 8 | exports.pkgRegex = /^(@[\w\.\-]+\/)?[\w\.\-]+$/; 9 | exports.fileRegex = /^\.?\.?\//; 10 | exports.whiteRegex = /^\s*$/; 11 | 12 | exports.readFile = promisify(fs.readFile, fs); 13 | exports.writeFile = promisify(fs.writeFile, fs); 14 | exports.stat = promisify(fs.stat, fs); 15 | exports.readdir = promisify(fs.readdir, fs); 16 | 17 | const metaPromises = {}; 18 | exports.findPkgMeta = function(dir) { 19 | if (!path.isAbsolute(dir)) { 20 | dir = path.resolve(dir); 21 | } 22 | 23 | if (!metaPromises[dir]) { 24 | metaPromises[dir] = findPkgJSON(dir).then(({ root, contents }) => { 25 | const meta = contents ? exports.tryJSONParse(contents, {}) : {}; 26 | meta.root = root; 27 | 28 | return getEslintConfig(meta, dir).then(config => { 29 | meta.env = config.env || 30 | { 31 | node: true, 32 | browser: true, 33 | es6: true 34 | }; 35 | meta.globals = config.globals || {}; 36 | return meta; 37 | }); 38 | }); 39 | } 40 | 41 | return metaPromises[dir]; 42 | }; 43 | 44 | exports.tryJSONParse = function(contents, defaultValue) { 45 | try { 46 | return JSON.parse(contents) || defaultValue; 47 | } catch (err) { 48 | if (err instanceof SyntaxError && err.message.indexOf("JSON") !== -1) { 49 | return defaultValue; 50 | } 51 | throw err; 52 | } 53 | }; 54 | 55 | function findPkgJSON(dir) { 56 | if (!path.isAbsolute(dir)) { 57 | dir = path.resolve(dir); 58 | } 59 | 60 | return exports 61 | .readFile(path.join(dir, "package.json"), "utf8") 62 | .then(contents => ({ root: dir, contents })) 63 | .catch(err => { 64 | if (err.code === "ENOENT") { 65 | const parent = path.join(dir, ".."); 66 | if (parent === dir) { 67 | // at the root; couldn't find anything 68 | return { root: null, contents: null }; 69 | } 70 | return findPkgJSON(parent); 71 | } else { 72 | throw err; 73 | } 74 | }); 75 | } 76 | 77 | const eslintPriority = { 78 | ".eslintrc.js": 5, 79 | ".eslintrc.yaml": 4, 80 | ".eslintrc.yml": 3, 81 | ".eslintrc.json": 2, 82 | ".eslintrc": 1, 83 | "package.json": 0 84 | }; 85 | const eslintFiles = new Set(Object.keys(eslintPriority)); 86 | 87 | function getEslintConfig(meta, dir) { 88 | if (!path.isAbsolute(dir)) { 89 | dir = path.resolve(dir); 90 | } 91 | 92 | return exports 93 | .readdir(dir) 94 | .then(files => { 95 | const configFiles = files.filter(f => eslintFiles.has(f)); 96 | if (configFiles.length === 0) { 97 | return null; 98 | } 99 | 100 | const bestFile = configFiles.reduce( 101 | (best, file) => 102 | eslintPriority[file] > eslintPriority[best] ? file : best, 103 | configFiles[0] 104 | ); 105 | const bestFilePath = path.join(dir, bestFile); 106 | 107 | return exports.readFile(bestFilePath, "utf8").then( 108 | contents => parseConfig(bestFilePath, contents), 109 | // ignore errors if we can't read the file 110 | () => null 111 | ); 112 | }) 113 | .then(config => { 114 | if (config) { 115 | return config; 116 | } 117 | 118 | const parent = path.join(dir, ".."); 119 | if (parent === dir) { 120 | return { 121 | env: null, 122 | globals: null 123 | }; 124 | } 125 | return getEslintConfig(meta, parent); 126 | }); 127 | } 128 | 129 | function parseConfig(filePath, contents) { 130 | let config; 131 | switch (path.basename(filePath)) { 132 | case ".eslintrc.js": 133 | return require(filePath); 134 | 135 | case ".eslintrc.yaml": 136 | case ".eslintrc.yml": 137 | return tryYAMLParse(contents, null); 138 | 139 | case ".eslintrc.json": 140 | return exports.tryJSONParse(contents, null); 141 | 142 | case ".eslintrc": 143 | config = exports.tryJSONParse(contents, null); 144 | if (config) { 145 | return config; 146 | } 147 | return tryYAMLParse(contents, null); 148 | 149 | case "package.json": 150 | return exports.tryJSONParse(contents, {}).eslintConfig || null; 151 | 152 | default: 153 | throw new Error(`bad eslint file: ${filePath}`); 154 | } 155 | } 156 | 157 | function tryYAMLParse(contents, defaultValue) { 158 | try { 159 | return yaml.safeLoad(contents) || defaultValue; 160 | } catch (err) { 161 | if (err instanceof yaml.YAMLException) { 162 | return defaultValue; 163 | } 164 | throw err; 165 | } 166 | } 167 | 168 | function promisify(fn, context) { 169 | return function(...args) { 170 | return new Promise((resolve, reject) => { 171 | args.push(function(err, ...callbackArgs) { 172 | if (err) { 173 | reject(err); 174 | } else { 175 | if (callbackArgs.length === 0) { 176 | resolve(); 177 | } else if (callbackArgs.length === 1) { 178 | resolve(callbackArgs[0]); 179 | } else { 180 | resolve(callbackArgs); 181 | } 182 | } 183 | }); 184 | 185 | fn.apply(context, args); 186 | }); 187 | }; 188 | } 189 | -------------------------------------------------------------------------------- /lib/dep-registry.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { spawn } = require("child_process"); 4 | const crypto = require("crypto"); 5 | const https = require("https"); 6 | const os = require("os"); 7 | const path = require("path"); 8 | const qs = require("querystring"); 9 | const repl = require("repl"); 10 | const util = require("util"); 11 | 12 | const { 13 | debug, 14 | pkgRegex, 15 | readFile, 16 | readdir, 17 | stat, 18 | tryJSONParse, 19 | writeFile 20 | } = require("./common"); 21 | const parser = require("./parser"); 22 | const ProgressBar = require("../patches/progress.js"); 23 | 24 | const identRegex = /^[$a-z_][0-9a-z_$]*$/i; 25 | const nonWordRegex = /[\W_]+/g; 26 | const propsScript = [ 27 | 'const object = require("%s");', 28 | "const json = JSON.stringify({", 29 | " props: Object.keys(object),", 30 | " hasDefault: Boolean(object.__esModule) && Boolean(object.default)", 31 | "});", 32 | "console.log(json);" 33 | ].join(""); 34 | 35 | const isNodeID = {}; 36 | repl._builtinLibs.forEach(id => isNodeID[id] = true); 37 | 38 | const cacheVersion = "v2"; 39 | 40 | class DepRegistry { 41 | /* public interface */ 42 | 43 | static populate(dir, meta) { 44 | if (this.promises && this.promises[dir]) { 45 | return this.promises[dir].then(instance => instance.copy(meta.env)); 46 | } 47 | 48 | const instance = new DepRegistry(meta.env); 49 | this.promises = this.promises || {}; 50 | this.promises[dir] = instance.populate(meta).then(() => instance); 51 | return this.promises[dir]; 52 | } 53 | 54 | constructor(env = null, registry = null, deps = null) { 55 | this.env = env; 56 | 57 | // registry[id] is an entry that has the given id 58 | this.registry = registry || {}; 59 | 60 | // deps[name] is a dep that corresponds to the identifier name 61 | this.deps = deps || {}; 62 | } 63 | 64 | search(name) { 65 | const dep = this.deps[name]; 66 | if (dep && !this.env.node && isNodeID[dep.id]) { 67 | return null; 68 | } 69 | return dep; 70 | } 71 | 72 | /* private interface */ 73 | 74 | copy(env) { 75 | return new DepRegistry(env, this.registry, this.deps); 76 | } 77 | 78 | populate(meta) { 79 | const hash = crypto.createHash("sha256"); 80 | hash.update(meta.root || "-"); 81 | const cachePath = path.join( 82 | os.tmpdir(), 83 | `tradeship-dir-${hash.digest("hex")}` 84 | ); 85 | 86 | return Promise 87 | .all([this.readCache(cachePath), this.findSourceFiles(meta.root)]) 88 | .then(([cache, sourceFiles]) => { 89 | this.registry._version = cache._version; 90 | sourceFiles = sourceFiles || []; 91 | 92 | const dependencies = Object.assign( 93 | {}, 94 | meta.devDependencies, 95 | meta.dependencies 96 | ); 97 | const total = repl._builtinLibs.length + 98 | Object.keys(dependencies).length + 99 | sourceFiles.length; 100 | const progress = new ProgressBar( 101 | "Populating dependencies [:bar] :percent :current/:total", 102 | { 103 | total, 104 | incomplete: " ", 105 | width: Math.min(total, 40), 106 | clear: true 107 | } 108 | ); 109 | 110 | const promises = []; 111 | const tick = () => progress.tick(); 112 | const entriesByID = {}; 113 | 114 | repl._builtinLibs.forEach(id => { 115 | const entry = this.register(cache, id, process.version); 116 | if (entry) { 117 | this.populateIdents(entry, id); 118 | promises.push(this.populatePropsDefaults(entry, id).then(tick)); 119 | entriesByID[id] = entry; 120 | } else { 121 | progress.tick(); 122 | } 123 | }); 124 | 125 | for (const id in dependencies) { 126 | const entry = this.register(cache, id, dependencies[id]); 127 | if (entry) { 128 | this.populateIdents(entry, id); 129 | const absPath = path.join(meta.root, "node_modules", id); 130 | 131 | promises.push( 132 | this.populatePropsDefaults(entry, absPath).then(tick) 133 | ); 134 | entriesByID[id] = entry; 135 | } else { 136 | progress.tick(); 137 | } 138 | } 139 | 140 | promises.push(this.populateDockIdents(entriesByID)); 141 | 142 | sourceFiles.forEach(sf => { 143 | const entry = this.register(cache, sf.path, sf.version); 144 | if (entry) { 145 | promises.push(this.populateFile(entry, sf.path).then(tick)); 146 | } else { 147 | progress.tick(); 148 | } 149 | }); 150 | 151 | return Promise.all(promises); 152 | }) 153 | .then(() => writeFile(cachePath, JSON.stringify(this.registry))) 154 | .then(() => this.computeDeps()); 155 | } 156 | 157 | readCache(cachePath) { 158 | return readFile(cachePath, "utf8") 159 | .catch(err => { 160 | if (err.code === "ENOENT") { 161 | return null; 162 | } 163 | throw err; 164 | }) 165 | .then(contents => { 166 | const cache = tryJSONParse(contents, {}); 167 | if (cache._version !== cacheVersion) { 168 | // reset cache, setting the new version 169 | return { _version: cacheVersion }; 170 | } 171 | return cache; 172 | }); 173 | } 174 | 175 | populateIdents(entry, id) { 176 | const ext = path.extname(id); 177 | let base; 178 | if (ext === ".js" || ext === ".jsx") { 179 | base = path.basename(id, ext); 180 | } else { 181 | base = path.basename(id); 182 | } 183 | 184 | if (identRegex.test(base)) { 185 | entry.idents.push(base); 186 | } 187 | 188 | const camelCase = base 189 | .split(nonWordRegex) 190 | .filter(p => p !== "") 191 | .map((p, i) => i === 0 ? p : p[0].toUpperCase() + p.slice(1)) 192 | .join(""); 193 | 194 | if (camelCase.length > 0) { 195 | if (camelCase !== base) { 196 | entry.idents.push(camelCase); 197 | } 198 | 199 | const classCase = camelCase[0].toUpperCase() + camelCase.slice(1); 200 | entry.idents.push(classCase); 201 | } 202 | } 203 | 204 | populatePropsDefaults(entry, id) { 205 | const escapedID = id.replace(/"/g, '\\"'); 206 | const cmd = spawn("node", ["-e", util.format(propsScript, escapedID)]); 207 | 208 | const promise = new Promise((resolve, reject) => { 209 | let stdout = ""; 210 | let stderr = ""; 211 | 212 | cmd.stdout.on("data", chunk => stdout += chunk); 213 | cmd.stderr.on("data", chunk => stderr += chunk); 214 | 215 | cmd.on("close", code => { 216 | if (code === 0) { 217 | resolve(stdout); 218 | } else { 219 | reject(new Error( 220 | `bad code: ${code}; stderr: ${stderr}`, 221 | code, 222 | stderr 223 | )); 224 | } 225 | }); 226 | cmd.on("error", err => reject(err)); 227 | }); 228 | 229 | return promise.then( 230 | json => { 231 | const { props, hasDefault } = tryJSONParse(json, {}); 232 | 233 | if (props) { 234 | props.forEach(p => entry.props.push(p)); 235 | } 236 | if (hasDefault) { 237 | entry.useDefault = true; 238 | } 239 | }, 240 | // ignore errors from requiring this dep; we just won't populate props 241 | err => debug("Couldn't populate props for %s: %O", path.basename(id), err) 242 | ); 243 | } 244 | 245 | populateDockIdents(entriesByID) { 246 | const ids = Object.keys(entriesByID); 247 | if (ids.length === 0) { 248 | return Promise.resolve(); 249 | } 250 | 251 | const options = { 252 | hostname: "dock.karthikv.net", 253 | path: `/load?${qs.stringify({ ids: ids.join(",") })}` 254 | }; 255 | 256 | return new Promise((resolve, reject) => { 257 | const request = https.request(options, response => { 258 | let body = ""; 259 | response.setEncoding("utf8"); 260 | response.on("data", chunk => body += chunk); 261 | response.on("end", () => resolve(body)); 262 | }); 263 | 264 | request.on("error", reject); 265 | request.end(); 266 | }).then(body => { 267 | const identMap = tryJSONParse(body, {}); 268 | for (const pkg in identMap) { 269 | const entry = entriesByID[pkg]; 270 | // take the top three most-frequent idents 271 | const idents = identMap[pkg].slice(0, 3).concat(entry.idents); 272 | entry.idents = Array.from(new Set(idents)); 273 | } 274 | }); 275 | } 276 | 277 | findSourceFiles(dir) { 278 | if (!dir) { 279 | return []; 280 | } 281 | 282 | return readdir(dir).then(files => { 283 | const sourceFiles = []; 284 | const promises = files.map(file => { 285 | if (file[0] === ".") { 286 | return Promise.resolve(); 287 | } 288 | 289 | const filePath = path.join(dir, file); 290 | return stat(filePath).then(s => { 291 | const ext = path.extname(file); 292 | 293 | if (s.isFile() && (ext === ".js" || ext === ".jsx")) { 294 | sourceFiles.push({ 295 | path: filePath, 296 | version: s.mtime.getTime() 297 | }); 298 | } else if ( 299 | s.isDirectory() && 300 | file !== "node_modules" && 301 | file !== "bower_components" 302 | ) { 303 | return this 304 | .findSourceFiles(filePath) 305 | .then(s => sourceFiles.push(...s)); 306 | } 307 | }); 308 | }); 309 | 310 | return Promise.all(promises).then(() => sourceFiles); 311 | }); 312 | } 313 | 314 | populateFile(entry, filePath) { 315 | return readFile(filePath, "utf8") 316 | .then(code => parser.run(path.dirname(filePath), code).catch(err => { 317 | debug("Parsing failed for %s: %O", filePath, err); 318 | return null; 319 | })) 320 | .then(exported => { 321 | if (!exported || !exported.hasExports) { 322 | return; 323 | } 324 | 325 | this.populateIdents(entry, filePath); 326 | exported.idents.forEach(i => entry.idents.push(i)); 327 | exported.props.forEach(p => entry.props.push(p)); 328 | 329 | if (exported.hasDefault) { 330 | entry.useDefault = true; 331 | } 332 | }); 333 | } 334 | 335 | register(cache, id, version) { 336 | if (!cache[id] || cache[id].version !== version) { 337 | const entry = { 338 | version, 339 | idents: [], 340 | props: [], 341 | useDefault: false 342 | }; 343 | this.registry[id] = entry; 344 | return entry; 345 | } else { 346 | this.registry[id] = cache[id]; 347 | // don't return entry; the cached data is valid 348 | return null; 349 | } 350 | } 351 | 352 | computeDeps() { 353 | for (const id in this.registry) { 354 | if (id === "_version") { 355 | continue; 356 | } 357 | 358 | let priority; 359 | if (isNodeID[id]) { 360 | // node library; lowest priority 361 | priority = 1; 362 | } else if (pkgRegex.test(id)) { 363 | // package.json dependency; middle priority 364 | priority = 2; 365 | } else { 366 | // project file; highest priority 367 | priority = 3; 368 | } 369 | 370 | const { idents, props, useDefault } = this.registry[id]; 371 | idents.forEach(name => this.associate({ 372 | name, 373 | id, 374 | priority, 375 | type: useDefault ? types.default : types.ident 376 | })); 377 | props.forEach(name => this.associate({ 378 | name, 379 | id, 380 | priority, 381 | type: types.prop 382 | })); 383 | } 384 | } 385 | 386 | associate({ name, id, priority, type }) { 387 | const dep = this.deps[name]; 388 | if ( 389 | !dep || 390 | dep.priority < priority || 391 | // idents and defaults are prioritized over props 392 | dep.priority === priority && 393 | dep.type === types.prop && 394 | type !== types.prop 395 | ) { 396 | this.deps[name] = { id, priority, type }; 397 | } 398 | } 399 | } 400 | 401 | DepRegistry.types = { 402 | ident: 1, 403 | default: 2, 404 | prop: 3 405 | }; 406 | const types = DepRegistry.types; 407 | 408 | module.exports = DepRegistry; 409 | -------------------------------------------------------------------------------- /lib/importer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | 5 | const { fileRegex, findPkgMeta, whiteRegex } = require("./common"); 6 | const DepRegistry = require("./dep-registry"); 7 | const findImports = require("../visits/find-imports.js"); 8 | const findStyle = require("../visits/find-style.js"); 9 | const resolveJSX = require("../visits/resolve-jsx.js"); 10 | const walker = require("./walker.js"); 11 | 12 | const backslashRegex = /\\/g; 13 | 14 | exports.run = function(dir, code, override) { 15 | return findPkgMeta(dir).then(meta => { 16 | if (override) { 17 | meta = Object.assign({}, meta, override); 18 | } 19 | 20 | const context = walker.run(meta, code, [ 21 | findImports, 22 | findStyle, 23 | resolveJSX 24 | ]); 25 | 26 | if (context.error) { 27 | throw context.error; 28 | } 29 | 30 | // resolve all relative dependency paths 31 | context.reqs.forEach(req => { 32 | if (fileRegex.test(req.depID)) { 33 | // must split on forward slash so resolving works correctly on Windows 34 | req.depID = path.resolve(dir, ...req.depID.split("/")); 35 | } 36 | }); 37 | 38 | return DepRegistry.populate(dir, meta) 39 | .then(depRegistry => rewriteCode(context, depRegistry, dir)); 40 | }); 41 | }; 42 | 43 | function rewriteCode(context, depRegistry, dir) { 44 | const { linesToRemove, libsToAdd } = resolveIdents(context, depRegistry); 45 | let requiresText = composeRequires(context.style, dir, libsToAdd); 46 | 47 | let newCode = ""; 48 | let targetLine = null; 49 | 50 | if (context.reqs.length > 0) { 51 | targetLine = context.reqs[0].node.loc.start.line; 52 | } else if (requiresText.length > 0) { 53 | const directive = context.getUseStrict(); 54 | 55 | if (directive) { 56 | if (context.endsLine(directive)) { 57 | requiresText = "\n" + requiresText + "\n"; 58 | targetLine = directive.loc.end.line; 59 | } else { 60 | const { loc: { end } } = directive; 61 | const endLineText = context.getLineText(end.line); 62 | 63 | // use strict is on the same line as some other text. Unfortunately, 64 | // with the current architecture, it's not easy to add the requires 65 | // between use strict and the other text, as we operate on a 66 | // line-by-line basis. Consequently, we resort to directly modifying 67 | // the line. This is ugly, but it's not worth changing the architecture 68 | // for a quite rare edge case like this one. Note that we can't add 69 | // a line to textLines because that would change line numbers and 70 | // break our logic below. 71 | context.textLines[end.line - 1] = endLineText.slice(0, end.column) + 72 | "\n\n" + 73 | requiresText + 74 | "\n\n" + 75 | endLineText.slice(end.column + 1); 76 | } 77 | } else { 78 | requiresText = requiresText + "\n"; 79 | targetLine = 0; 80 | } 81 | } 82 | 83 | // start at non-existent line 0 to allow requiresText to be prepended 84 | linesToRemove.add(0); 85 | for (let line = 0; line <= context.textLines.length; line++) { 86 | if (!linesToRemove.has(line)) { 87 | newCode += context.getLineText(line) + "\n"; 88 | } 89 | if (line === targetLine && requiresText.length > 0) { 90 | newCode += requiresText + "\n"; 91 | } 92 | } 93 | 94 | if (newCode.slice(-1) !== "\n") { 95 | newCode = newCode + "\n"; 96 | } else if (newCode.slice(-2) === "\n\n") { 97 | newCode = newCode.slice(0, -1); 98 | } 99 | return newCode; 100 | } 101 | 102 | function resolveIdents(context, depRegistry) { 103 | const missingIdents = findMissingIdents(context); 104 | const fixableIdents = missingIdents.filter(i => depRegistry.search(i)); 105 | 106 | const deps = fixableIdents.map(i => depRegistry.search(i)); 107 | const depIDs = context.reqs.map(req => req.depID).concat(deps.map(d => d.id)); 108 | 109 | const libsToAdd = {}; 110 | depIDs.forEach( 111 | id => libsToAdd[id] = { 112 | idents: [], 113 | defaults: [], 114 | props: [] 115 | } 116 | ); 117 | 118 | const { types } = DepRegistry; 119 | fixableIdents.forEach((ident, i) => { 120 | const { id, type } = deps[i]; 121 | const lib = libsToAdd[id]; 122 | 123 | switch (type) { 124 | case types.ident: 125 | lib.idents.push(ident); 126 | break; 127 | case types.default: 128 | lib.defaults.push(ident); 129 | break; 130 | case types.prop: 131 | lib.props.push(ident); 132 | break; 133 | default: 134 | throw new Error("unexpected type " + type); 135 | } 136 | }); 137 | 138 | const nodesToRemove = []; 139 | context.reqs.forEach(({ node, depID, idents, defaults, props }) => { 140 | const lib = libsToAdd[depID]; 141 | 142 | if (node) { 143 | nodesToRemove.push(node); 144 | } 145 | if (idents) { 146 | lib.idents.push(...idents); 147 | } 148 | if (defaults) { 149 | lib.defaults.push(...defaults); 150 | } 151 | if (props) { 152 | lib.props.push(...props); 153 | } 154 | }); 155 | 156 | const linesToRemove = new Set(); 157 | nodesToRemove.forEach(({ loc: { start, end } }) => { 158 | for (let line = start.line; line <= end.line; line++) { 159 | linesToRemove.add(line); 160 | } 161 | }); 162 | 163 | removeExtraLines(context, libsToAdd, linesToRemove); 164 | return { libsToAdd, linesToRemove }; 165 | } 166 | 167 | function findMissingIdents(context) { 168 | const globalScope = context.getGlobalScope(); 169 | const missingIdents = globalScope.through 170 | .filter(ref => { 171 | // ignore: 172 | // - identifiers prefixed with typeof 173 | // - writes to undeclared variables 174 | const parent = ref.identifier.parent; 175 | const isTypeOf = parent && 176 | parent.type === "UnaryExpression" && 177 | parent.operator === "typeof"; 178 | 179 | return !isTypeOf && !ref.writeExpr; 180 | }) 181 | .map(ref => ref.identifier.name) 182 | .filter(name => !globalScope.set.get(name)); 183 | return Array.from(new Set(missingIdents)); 184 | } 185 | 186 | function removeExtraLines(context, libsToAdd, linesToRemove) { 187 | const sortedLinesToRemove = Array.from(linesToRemove) 188 | .sort((l1, l2) => l1 - l2); 189 | let prevLine = sortedLinesToRemove[0]; 190 | 191 | // If the intermediate lines between two subsequent lines to remove are all 192 | // blank, remove the intermediate lines as well. 193 | sortedLinesToRemove.slice(1).forEach(line => { 194 | let allBlank = true; 195 | for (let j = prevLine + 1; j < line; j++) { 196 | allBlank = allBlank && whiteRegex.test(context.getLineText(j)); 197 | } 198 | 199 | if (allBlank) { 200 | for (let j = prevLine + 1; j < line; j++) { 201 | linesToRemove.add(j); 202 | } 203 | } 204 | prevLine = line; 205 | }); 206 | 207 | const hasNoImports = Object.keys(libsToAdd).every(id => { 208 | const { idents, defaults, props } = libsToAdd[id]; 209 | return idents.length === 0 && defaults.length === 0 && props.length === 0; 210 | }); 211 | 212 | // If we're removing all imports, remove the blank line after them. 213 | if ( 214 | hasNoImports && 215 | sortedLinesToRemove.length > 0 && 216 | whiteRegex.test(context.getLineText(prevLine + 1)) 217 | ) { 218 | linesToRemove.add(prevLine + 1); 219 | } 220 | } 221 | 222 | function composeRequires(style, dir, libs) { 223 | // turn absolute dep ids into relative ones 224 | Object.keys(libs).forEach(id => { 225 | if (path.isAbsolute(id)) { 226 | // node module ids always have unix-style separators 227 | let newID = path.relative(dir, id).replace(backslashRegex, "/"); 228 | if (newID[0] !== ".") { 229 | newID = `./${newID}`; 230 | } 231 | libs[newID] = libs[id]; 232 | delete libs[id]; 233 | } 234 | }); 235 | 236 | const ids = Object.keys(libs); 237 | const externalIDs = ids 238 | .filter(i => !fileRegex.test(i)) 239 | .sort(compareByBasename); 240 | const localIDs = ids.filter(i => fileRegex.test(i)).sort(compareByBasename); 241 | 242 | const externalStatements = []; 243 | const localStatements = []; 244 | 245 | externalIDs.forEach(id => 246 | externalStatements.push(...composeStatements(style, libs[id], id))); 247 | localIDs.forEach(id => 248 | localStatements.push(...composeStatements(style, libs[id], id))); 249 | 250 | const statements = externalStatements; 251 | if (externalStatements.length > 0 && localStatements.length > 0) { 252 | // add blank line between external and local imports 253 | statements.push(""); 254 | } 255 | statements.push(...localStatements); 256 | 257 | return statements.join("\n"); 258 | } 259 | 260 | function compareByBasename(id1, id2) { 261 | const base1 = path.basename(id1); 262 | const base2 = path.basename(id2); 263 | 264 | if (base1 !== base2) { 265 | return base1 < base2 ? -1 : 1; 266 | } 267 | return id1 < id2 ? -1 : 1; 268 | } 269 | 270 | function composeStatements(style, lib, id) { 271 | const statements = []; 272 | const { idents, defaults, props } = lib; 273 | 274 | if (idents.length === 0 && defaults.length === 0 && props.length === 0) { 275 | // nothing to require 276 | return statements; 277 | } 278 | 279 | idents.sort(); 280 | defaults.sort(); 281 | props.sort(); 282 | 283 | if (style.requireKeyword === "require") { 284 | statements.push( 285 | ...idents.map(ident => composeRequireStatement({ style, id, ident })), 286 | ...defaults.map(def => composeRequireStatement({ style, id, def })) 287 | ); 288 | 289 | if (props.length > 0) { 290 | statements.push(composeRequireStatement({ style, id, props })); 291 | } 292 | } else { 293 | let leftDefaults = defaults; 294 | if (props && props.length > 0) { 295 | statements.push( 296 | composeImportStatement({ style, id, props, def: defaults[0] }) 297 | ); 298 | leftDefaults = defaults.slice(1); 299 | } 300 | 301 | statements.push( 302 | ...leftDefaults.map((def, i) => 303 | composeImportStatement({ style, id, ident: idents[i], def })), 304 | ...idents 305 | .slice(leftDefaults.length) 306 | .map(ident => composeImportStatement({ style, id, ident })) 307 | ); 308 | } 309 | 310 | return statements; 311 | } 312 | 313 | function composeRequireStatement({ style, id, ident, def, props, multiline }) { 314 | if (ident && def || ident && props || def && props) { 315 | throw new Error("only one of ident, default, and props must be specified"); 316 | } 317 | 318 | const { kind, quote, semi } = style; 319 | const requireText = `require(${quote}${id}${quote})`; 320 | 321 | if (ident) { 322 | return `${kind} ${ident} = ${requireText}${semi}`; 323 | } else if (def) { 324 | return `${kind} ${def} = ${requireText}.default${semi}`; 325 | } else { 326 | const destructure = composeDestructure(style, props, multiline); 327 | const statement = `${kind} ${destructure} = ${requireText}${semi}`; 328 | 329 | if (!multiline && statement.length > 80) { 330 | return composeRequireStatement({ 331 | style, 332 | id, 333 | props, 334 | multiline: true 335 | }); 336 | } 337 | return statement; 338 | } 339 | } 340 | 341 | function composeImportStatement({ style, id, ident, def, props, multiline }) { 342 | if (ident && props && props.length > 0) { 343 | throw new Error("ident and props cannot both be specified"); 344 | } 345 | 346 | const parts = []; 347 | if (def) { 348 | parts.push(def); 349 | } 350 | if (ident) { 351 | parts.push(`* as ${ident}`); 352 | } 353 | if (props && props.length > 0) { 354 | parts.push(composeDestructure(style, props, multiline)); 355 | } 356 | 357 | const { quote, semi } = style; 358 | const names = parts.join(", "); 359 | const statement = `import ${names} from ${quote}${id}${quote}${semi}`; 360 | 361 | if (props && !multiline && statement.length > 80) { 362 | return composeImportStatement({ 363 | style, 364 | id, 365 | ident, 366 | def, 367 | props, 368 | multiline: true 369 | }); 370 | } 371 | return statement; 372 | } 373 | 374 | function composeDestructure(style, props, multiline) { 375 | if (multiline) { 376 | const { tab, trailingComma } = style; 377 | const propsText = tab + props.join(`,\n${tab}`) + trailingComma; 378 | return `{\n${propsText}\n}`; 379 | } else { 380 | return `{ ${props.join(", ")} }`; 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { findPkgMeta } = require("./common"); 4 | const findExports = require("../visits/find-exports.js"); 5 | const walker = require("./walker.js"); 6 | 7 | exports.run = function(dir, code) { 8 | return findPkgMeta(dir).then(meta => { 9 | const context = walker.run(meta, code, [findExports]); 10 | if (context.error) { 11 | throw context.error; 12 | } 13 | 14 | return context.exported; 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/walker.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const babylon = require("babylon"); 4 | const estraverse = require("estraverse"); 5 | const globals = require("globals"); 6 | 7 | const astHelpers = require("./ast-helpers.js"); 8 | const { whiteRegex } = require("./common.js"); 9 | const escope = require("../patches/escope.js"); 10 | 11 | const lineRegex = /\r\n|[\r\n\u2028\u2029]/g; 12 | 13 | exports.run = function(meta, code, visits) { 14 | let ast, scopeManager, fallback; 15 | try { 16 | ({ ast, scopeManager, fallback } = parse(code)); 17 | } catch (err) { 18 | if (err instanceof SyntaxError) { 19 | return { error: err }; 20 | } 21 | throw err; 22 | } 23 | 24 | const context = createContext(ast, scopeManager, code); 25 | populateGlobals(meta, scopeManager); 26 | 27 | const callbacks = {}; 28 | visits.forEach(visit => { 29 | const map = visit.init(context); 30 | for (const type in map) { 31 | callbacks[type] = callbacks[type] || []; 32 | callbacks[type].push(map[type]); 33 | } 34 | }); 35 | 36 | estraverse.traverse(ast.program, { 37 | fallback, 38 | enter(node, parent) { 39 | node.parent = parent; 40 | context.curNode = node; 41 | if (callbacks[node.type]) { 42 | callbacks[node.type].forEach(fn => fn(node)); 43 | } 44 | } 45 | }); 46 | 47 | if (callbacks.finish) { 48 | callbacks.finish.forEach(fn => fn()); 49 | } 50 | return context; 51 | }; 52 | 53 | function parse(code) { 54 | const ast = babylon.parse(code, { 55 | sourceType: "module", 56 | allowImportExportEverywhere: false, 57 | allowReturnOutsideFunction: true, 58 | plugins: [ 59 | "estree", 60 | "jsx", 61 | "flow", 62 | "doExpressions", 63 | "objectRestSpread", 64 | "decorators", 65 | "classProperties", 66 | "exportExtensions", 67 | "asyncGenerators", 68 | "functionBind", 69 | "functionSent", 70 | "dynamicImport" 71 | ] 72 | }); 73 | 74 | const fallback = function(node) { 75 | if (astHelpers.isFlowNode(node)) { 76 | return []; 77 | } 78 | return Object.keys(node).filter(k => k !== "parent"); 79 | }; 80 | 81 | const scopeManager = escope.analyze(ast, { 82 | fallback, 83 | ecmaVersion: 6, 84 | nodejsScope: false, 85 | impliedStrict: false, 86 | ignoreEval: true, 87 | sourceType: "module" 88 | }); 89 | 90 | return { ast, scopeManager, fallback }; 91 | } 92 | 93 | function createContext(ast, scopeManager, code) { 94 | return { 95 | curNode: null, 96 | textLines: code.split(lineRegex), 97 | 98 | getLineText(line) { 99 | // line numbers start from 1 100 | return this.textLines[line - 1]; 101 | }, 102 | 103 | getLastToken(node, skip = 0) { 104 | const index = ast.tokens.findIndex( 105 | t => t.start < node.end && t.end >= node.end 106 | ); 107 | return ast.tokens[index - skip]; 108 | }, 109 | 110 | getTokenAfter(node) { 111 | return ast.tokens.find(t => t.start > node.end); 112 | }, 113 | 114 | getGlobalScope() { 115 | return scopeManager.globalScope; 116 | }, 117 | 118 | getKey(node) { 119 | switch (node.type) { 120 | case "Identifier": 121 | return node.name; 122 | 123 | case "StringLiteral": 124 | return node.value; 125 | 126 | default: 127 | return null; 128 | } 129 | }, 130 | 131 | getScope() { 132 | let scope = null; 133 | let node = this.curNode; 134 | 135 | do { 136 | scope = scopeManager.acquire(node); 137 | node = node.parent; 138 | } while (!scope); 139 | 140 | // top-most scope should be module scope, not global scope 141 | if (scope.type === "global" && scope.childScopes.length === 1) { 142 | scope = scope.childScopes[0]; 143 | } 144 | return scope; 145 | }, 146 | 147 | findVariable(name) { 148 | if (name.type === "Identifier" || name.type === "JSXIdentifier") { 149 | name = name.name; 150 | } 151 | 152 | let scope = this.getScope(); 153 | do { 154 | const variable = scope.set.get(name); 155 | if (variable) { 156 | return variable; 157 | } 158 | scope = scope.upper; 159 | } while (scope); 160 | 161 | return null; 162 | }, 163 | 164 | isGlobal(node, name) { 165 | if (node.type === "Identifier" && node.name === name) { 166 | const variable = this.findVariable(node); 167 | return variable && variable.scope.type === "global"; 168 | } 169 | return false; 170 | }, 171 | 172 | startsLine(node) { 173 | const { loc: { start } } = node; 174 | const startLineText = this.getLineText(start.line); 175 | return whiteRegex.test(startLineText.slice(0, start.column)); 176 | }, 177 | 178 | endsLine(node) { 179 | const { loc: { end } } = node; 180 | const endLineText = this.getLineText(end.line); 181 | return whiteRegex.test(endLineText.slice(end.column)); 182 | }, 183 | 184 | getUseStrict() { 185 | const node = ast.program.body[0]; 186 | if ( 187 | node && 188 | node.type === "ExpressionStatement" && 189 | node.directive === "use strict" 190 | ) { 191 | return node; 192 | } 193 | return null; 194 | } 195 | }; 196 | } 197 | 198 | function populateGlobals(meta, scopeManager) { 199 | const globalNames = new Set(); 200 | Object.keys(meta.globals).forEach(g => globalNames.add(g)); 201 | 202 | const envNames = ["builtin"]; 203 | for (const name in meta.env) { 204 | if (meta.env[name]) { 205 | envNames.push(name); 206 | } 207 | } 208 | 209 | envNames.forEach(e => 210 | Object.keys(globals[e]).forEach(g => globalNames.add(g))); 211 | 212 | // to identify imports and exports, we must have these globals 213 | ["require", "module", "exports"].forEach(g => globalNames.add(g)); 214 | 215 | const globalScope = scopeManager.globalScope; 216 | globalNames.forEach(name => { 217 | const variable = globalScope.set.get(name); 218 | 219 | if (!variable) { 220 | const newVariable = new escope.Variable(name, globalScope); 221 | globalScope.variables.push(newVariable); 222 | globalScope.set.set(name, newVariable); 223 | } 224 | }); 225 | } 226 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karthikv/tradeship/f91740b5d35cf1ee144b073e17b1a5fa2919583e/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tradeship", 3 | "description": "Automatically imports missing JS dependencies and removes unused ones.", 4 | "author": "Karthik ViswanathanParagraph
; 336 | expected: | 337 | const React = require("react"); 338 |Paragraph
; 339 | 340 | - name: keep-jsx-ident 341 | node: false 342 | input: | 343 | const React = require("react"); 344 | 345 | const Component = require("../fixtures/component.js"); 346 | 347 |Paragraph
; 506 | expected: | 507 | const React = require("react"); 508 | 509 |Paragraph
; 510 | 511 | - name: jsx-ident 512 | node: false 513 | input: | 514 |