diff --git a/.editorconfig b/.editorconfig index 90913036db..e2bfac523f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,8 +4,6 @@ root = true charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true - -[*.js] indent_style = space -indent_width = 2 +indent_size = 2 end_of_line = lf diff --git a/.eslintrc b/.eslintrc index 230d901550..f27ff79e4a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -12,3 +12,4 @@ rules: curly: [2, "multi-line"] comma-dangle: [2, always-multiline] eqeqeq: [2, "allow-null"] + no-shadow: 1 diff --git a/.npmignore b/.npmignore index 34c638a583..eeb6ce496e 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,7 @@ # files src/ reports/ +resolvers/ # config .eslintrc diff --git a/.travis.yml b/.travis.yml index c34ac82f05..b7324c425d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,21 @@ language: node_js node_js: - 0.10 - 0.12 - - iojs + - 4 - stable env: - NODE_PATH=./lib -script: "npm run-script ci-test" +install: + - npm -g install npm@2 + - npm install + # install all resolver deps + - "for resolver in ./resolvers/*; do cd $resolver && npm install && cd ../..; done" + +script: + - "npm run-script ci-test" + - "cd resolvers/webpack && npm test" + - "cd ../.." # come back out of resolvers folder after_success: - npm run coveralls diff --git a/README.md b/README.md index 918e081256..6348d0c92e 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ rules: ``` -## Rule Details +# Rule Details ### `no-unresolved` @@ -79,6 +79,8 @@ This rule can also optionally report on unresolved modules in CommonJS `require( To enable this, send `{ commonjs: true/false, amd: true/false }` as a rule option. Both are disabled by default. +If you are using Webpack, see the section on [resolver plugins](#resolver-plugins). + ### `named` Verifies that all named imports are part of the set of named exports in the referenced module. @@ -254,8 +256,85 @@ import * as _ from 'lodash' // <- reported This rule is disabled by default. +# Resolver plugins + +With the advent of module bundlers and the current state of modules and module +syntax specs, it's not always obvious where `import x from 'module'` should look +to find the file behind `module`. + +Up through v0.10ish, this plugin has directly used substack's [`resolve`] plugin, +which implements Node's import behavior. This works pretty well in most cases. + +However, Webpack allows a number of things in import module source strings that +Node does not, such as loaders (`import 'file!./whatever'`) and a number of +aliasing schemes, such as [`externals`]: mapping a module id to a global name at +runtime (allowing some modules to be included more traditionally via script tags). + +In the interest of supporting both of these, v0.11 introduces resolver plugins. +At the moment, these are modules exporting a single function: + +```js + +exports.resolveImport = function (source, file, config) { + // return source's absolute path given + // - file: absolute path of importing module + // - config: optional config provided for this resolver + + // return `null` if source is a "core" module (i.e. "fs", "crypto") that + // can't be found on the filesystem +} +``` + +The default `node` plugin that uses [`resolve`] is a handful of lines: + +```js +var resolve = require('resolve') + , path = require('path') + , assign = require('object-assign') + +exports.resolveImport = function resolveImport(source, file, config) { + if (resolve.isCore(source)) return null + + return resolve.sync(source, opts(path.dirname(file), config)) +} + +function opts(basedir, config) { + return assign( {} + , config + , { basedir: basedir } + ) +} +``` + +It essentially just uses the current file to get a reference base directory (`basedir`) +and then passes through any explicit config from the `.eslintrc`; things like +non-standard file extensions, module directories, etc. + +Currently [Node] and [Webpack] resolution have been implemented, but the +resolvers are just npm packages, so third party packages are supported (and encouraged!). + +Just install a resolver as `eslint-import-resolver-foo` and reference it as such: + +```yaml +settings: + import/resolver: foo +``` + +or with a config object: + +```yaml +settings: + import/resolver: + foo: { someConfigKey: value } +``` + +[`resolve`]: https://www.npmjs.com/package/resolve +[`externals`]: http://webpack.github.io/docs/library-and-externals.html -## Settings +[Node]: https://www.npmjs.com/package/eslint-import-resolver-node +[Webpack]: https://www.npmjs.com/package/eslint-import-resolver-webpack + +# Settings You may set the following settings in your `.eslintrc`: @@ -265,11 +344,9 @@ A list of regex strings that, if matched by a path, will not parse the matching module. In practice, this means rules other than `no-unresolved` will not report on the `import` in question. -#### `import/resolve` - -A passthrough to [resolve]'s `opts` parameter for `resolve.sync`. +#### `import/resolver` -[resolve]: https://www.npmjs.com/package/resolve#resolve-sync-id-opts +See [resolver plugins](#resolver-plugins). #### `import/parser` @@ -320,31 +397,9 @@ settings: - 'node_modules' # this is the default, but must be included if overwritten - '\\.es5$' - import/resolve: - - extensions: - # if unset, default is just '.js', but it must be re-added explicitly if set - - .js - - .jsx - - .es6 - - .coffee - - paths: - # an array of absolute paths which will also be searched - # think NODE_PATH - - /usr/local/share/global_modules - - # this is technically for identifying `node_modules` alternate names - moduleDirectory: - - - node_modules # defaults to 'node_modules', but... - - bower_components - - - project/src # can add a path segment here that will act like - # a source root, for in-project aliasing (i.e. - # `import MyStore from 'stores/my-store'`) + import/resolver: webpack # will use 'node' if not specified - import/parser: esprima-fb # default is 'babel-core'. change if needed. + import/parser: esprima-fb # default is 'babylon'. change if needed. ``` ## SublimeLinter-eslint diff --git a/appveyor.yml b/appveyor.yml index 7dcd97210a..8c790668ba 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,6 +18,11 @@ environment: install: # Get the latest stable version of Node.js or io.js - ps: Install-Product node $env:nodejs_version + + # update npm + - npm -g install npm@2 + - set PATH=%APPDATA%\npm;%PATH% + # install modules - npm install diff --git a/package.json b/package.json index 530c0a86bf..479357b95c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-import", - "version": "0.10.1", + "version": "0.11.0", "description": "Import with sanity.", "main": "lib/index.js", "directories": { @@ -13,8 +13,8 @@ "ci-test": "npm run-script pretest && mocha --recursive --reporter dot tests/lib/", "debug": "mocha debug --recursive --reporter dot tests/lib/", "compile": "rm -rf lib/ && babel -d lib/ src/", - "prepublish": "npm run compile", - "pretest": "eslint ./src && npm run compile && babel -d tests/lib/ tests/src/", + "prepublish": "eslint ./src && npm run compile", + "pretest": "npm run compile && babel -d tests/lib/ tests/src/", "coveralls": "istanbul cover --report lcovonly --dir reports/coverage _mocha tests/lib/ -- --recursive --reporter dot; remap-istanbul -i reports/coverage/coverage.json -o reports/coverage/lcov.info --type lcovonly && cat ./reports/coverage/lcov.info | coveralls" }, "repository": { @@ -44,7 +44,9 @@ "eslint": ">=1.8.0", "istanbul": "^0.4.0", "mocha": "^2.2.1", - "remap-istanbul": "^0.4.0" + "remap-istanbul": "^0.4.0", + "eslint-import-resolver-webpack": "file:./resolvers/webpack", + "eslint-import-resolver-node": "file:./resolvers/node" }, "peerDependencies": { "eslint": ">=1.4.0" @@ -52,7 +54,7 @@ "dependencies": { "babel-runtime": "5.x", "babylon": "^6.1.2", - "resolve": "1.1.6" + "eslint-import-resolver-node": "^0.1.0" }, "greenkeeper": { "ignore": [ diff --git a/resolvers/.eslintrc b/resolvers/.eslintrc new file mode 100644 index 0000000000..9db33eda46 --- /dev/null +++ b/resolvers/.eslintrc @@ -0,0 +1,3 @@ +--- +env: + es6: false diff --git a/resolvers/node/README.md b/resolvers/node/README.md new file mode 100644 index 0000000000..20fe1e5af4 --- /dev/null +++ b/resolvers/node/README.md @@ -0,0 +1,44 @@ +# eslint-import-resolver-node + +[![npm](https://img.shields.io/npm/v/eslint-import-resolver-node.svg)](https://www.npmjs.com/package/eslint-import-resolver-node) + +Default Node-style module resolution plugin for [`eslint-plugin-import`](https://www.npmjs.com/package/eslint-plugin-import). + +Published separately to allow pegging to a specific version in case of breaking +changes. + +Config is passed directly through to [`resolve`](https://www.npmjs.com/package/resolve#resolve-sync-id-opts) as options: + +```yaml +settings: + import/resolver: + node: + extensions: + # if unset, default is just '.js', but it must be re-added explicitly if set + - .js + - .jsx + - .es6 + - .coffee + + paths: + # an array of absolute paths which will also be searched + # think NODE_PATH + - /usr/local/share/global_modules + + # this is technically for identifying `node_modules` alternate names + moduleDirectory: + + - node_modules # defaults to 'node_modules', but... + - bower_components + + - project/src # can add a path segment here that will act like + # a source root, for in-project aliasing (i.e. + # `import MyStore from 'stores/my-store'`) +``` + +or to use the default options: + +```yaml +settings: + import/resolver: node +``` diff --git a/resolvers/node/index.js b/resolvers/node/index.js new file mode 100644 index 0000000000..6cb03d8847 --- /dev/null +++ b/resolvers/node/index.js @@ -0,0 +1,17 @@ +var resolve = require('resolve') + , path = require('path') + , assign = require('object-assign') + +exports.resolveImport = function resolveImport(source, file, config) { + if (resolve.isCore(source)) return null + + return resolve.sync(source, opts(path.dirname(file), config)) +} + +function opts(basedir, config) { + return assign( {} + , config + , { basedir: basedir } + ) +} + diff --git a/resolvers/node/package.json b/resolvers/node/package.json new file mode 100644 index 0000000000..bf8266e374 --- /dev/null +++ b/resolvers/node/package.json @@ -0,0 +1,30 @@ +{ + "name": "eslint-import-resolver-node", + "version": "0.1.0", + "description": "Node default behavior import resolution plugin for eslint-plugin-import.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/benmosher/eslint-plugin-import" + }, + "keywords": [ + "eslint", + "eslintplugin", + "esnext", + "modules", + "eslint-plugin-import" + ], + "author": "Ben Mosher (me@benmosher.com)", + "license": "MIT", + "bugs": { + "url": "https://github.com/benmosher/eslint-plugin-import/issues" + }, + "homepage": "https://github.com/benmosher/eslint-plugin-import", + "dependencies": { + "object-assign": "^4.0.1", + "resolve": "^1.1.6" + } +} diff --git a/resolvers/webpack/.babelrc b/resolvers/webpack/.babelrc new file mode 100644 index 0000000000..7a870ac678 --- /dev/null +++ b/resolvers/webpack/.babelrc @@ -0,0 +1 @@ +{ "presets": ["es2015"] } \ No newline at end of file diff --git a/resolvers/webpack/.npmignore b/resolvers/webpack/.npmignore new file mode 100644 index 0000000000..e9a16cce02 --- /dev/null +++ b/resolvers/webpack/.npmignore @@ -0,0 +1,2 @@ +test/ +.babelrc diff --git a/resolvers/webpack/README.md b/resolvers/webpack/README.md new file mode 100644 index 0000000000..8c24a2781c --- /dev/null +++ b/resolvers/webpack/README.md @@ -0,0 +1,27 @@ +# eslint-import-resolver-webpack + +[![npm](https://img.shields.io/npm/v/eslint-import-resolver-webpack.svg)](https://www.npmjs.com/package/eslint-import-resolver-webpack) + +Webpack-literate module resolution plugin for [`eslint-plugin-import`](https://www.npmjs.com/package/eslint-plugin-import). + +Published separately to allow pegging to a specific version in case of breaking +changes. + +Will look for `webpack.config.js` as a sibling of the first ancestral `package.json`, +or a `config` parameter may be provided with another filename/path relative to the +`package.json`. + +```yaml +--- +settings: + import/resolver: webpack # take all defaults +``` + +or with explicit config file name: + +```yaml +--- +settings: + import/resolver: + webpack: { config: 'webpack.dev.config.js' } +``` diff --git a/resolvers/webpack/index.js b/resolvers/webpack/index.js new file mode 100644 index 0000000000..26b0cb6072 --- /dev/null +++ b/resolvers/webpack/index.js @@ -0,0 +1,87 @@ +var findRoot = require('find-root') + , path = require('path') + , resolve = require('resolve') + , get = require('lodash.get') + +var resolveAlias = require('./resolve-alias') + +/** + * Find the full path to 'source', given 'file' as a full reference path. + * + * resolveImport('./foo', '/Users/ben/bar.js') => '/Users/ben/foo.js' + * @param {string} source - the module to resolve; i.e './some-module' + * @param {string} file - the importing file's full path; i.e. '/usr/local/bin/file.js' + * TODO: take options as a third param, with webpack config file name + * @return {string?} the resolved path to source, undefined if not resolved, or null + * if resolved to a non-FS resource (i.e. script tag at page load) + */ +exports.resolveImport = function resolveImport(source, file, settings) { + + // strip loaders + var finalBang = source.lastIndexOf('!') + if (finalBang >= 0) { + source = source.slice(finalBang + 1) + } + + if (resolve.isCore(source)) return null + + var webpackConfig + try { + var packageDir = findRoot(file) + if (!packageDir) throw new Error('package not found above ' + file) + + webpackConfig = require(path.join(packageDir, get(settings, 'config', 'webpack.config.js'))) + } catch (err) { + webpackConfig = {} + } + + // externals + if (findExternal(source, webpackConfig.externals)) return null + + // replace alias if needed + source = resolveAlias(source, get(webpackConfig, ['resolve', 'alias'], {})) + + var paths = [] + + // root as first alternate path + var rootPath = get(webpackConfig, ['resolve', 'root']) + if (rootPath) paths.push(rootPath) + + // otherwise, resolve "normally" + return resolve.sync(source, { + basedir: path.dirname(file), + + // defined via http://webpack.github.io/docs/configuration.html#resolve-extensions + extensions: get(webpackConfig, ['resolve', 'extensions']) + || ['', '.webpack.js', '.web.js', '.js'], + + // http://webpack.github.io/docs/configuration.html#resolve-modulesdirectories + moduleDirectory: get(webpackConfig, ['resolve', 'modulesDirectories']) + || ['web_modules', 'node_modules'], + + paths: paths, + }) +} + +function findExternal(source, externals) { + if (!externals) return false + + // string match + if (typeof externals === 'string') return (source === externals) + + // array: recurse + if (externals instanceof Array) { + return externals.some(function (e) { return findExternal(source, e) }) + } + + if (externals instanceof RegExp) { + return externals.test(source) + } + + if (typeof externals === 'function') { + throw new Error('unable to handle function externals') + } + + // else, vanilla object + return Object.keys(externals).some(function (e) { return source === e }) +} diff --git a/resolvers/webpack/package.json b/resolvers/webpack/package.json new file mode 100644 index 0000000000..c6a231bb4c --- /dev/null +++ b/resolvers/webpack/package.json @@ -0,0 +1,37 @@ +{ + "name": "eslint-import-resolver-webpack", + "version": "0.1.0", + "description": "Resolve paths to dependencies, given a webpack.config.js. Plugin for eslint-plugin-import.", + "main": "index.js", + "scripts": { + "test": "mocha --compilers js:babel-core/register" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/benmosher/eslint-plugin-import.git" + }, + "keywords": [ + "eslint-plugin-import", + "eslint", + "jsnext", + "modules", + "webpack" + ], + "author": "Ben Mosher (me@benmosher.com)", + "license": "MIT", + "bugs": { + "url": "https://github.com/benmosher/eslint-plugin-import/issues" + }, + "homepage": "https://github.com/benmosher/eslint-plugin-import#readme", + "dependencies": { + "find-root": "^0.1.1", + "lodash.get": "^3.7.0", + "resolve": "^1.1.6" + }, + "devDependencies": { + "babel-core": "^6.1.21", + "babel-preset-es2015": "^6.1.18", + "chai": "^3.4.1", + "mocha": "^2.3.3" + } +} diff --git a/resolvers/webpack/resolve-alias.js b/resolvers/webpack/resolve-alias.js new file mode 100644 index 0000000000..1ebcd26516 --- /dev/null +++ b/resolvers/webpack/resolve-alias.js @@ -0,0 +1,36 @@ +var path = require('path') + +// implements a rough version of +// http://webpack.github.io/docs/configuration.html#resolve-alias +module.exports = function resolveAlias(source, aliases) { + + for (var alias in aliases) { + var match = matchAlias(source, alias, aliases[alias]) + if (match) return match + } + + // fail out + return source +} + +function matchAlias(source, alias, value) { + var isExact = (alias[alias.length - 1] === '$') + , isFile = (path.extname(value) !== '') + , segments = source.split(path.sep) + + if (isExact) alias = alias.slice(0, -1) + + if (segments[0] === alias) { + // always return exact match + if (segments.length === 1) return value + + // prefix match on exact match for file is an error + if (isFile && (isExact || !/^[./]/.test(value))) { + throw new Error('can\'t match file with exact alias prefix') + } + + // otherwise, prefix match is fine for non-file paths + if (!isExact && !isFile) return [value].concat(segments.slice(1)).join(path.sep) + } + +} diff --git a/resolvers/webpack/test/.eslintrc b/resolvers/webpack/test/.eslintrc new file mode 100644 index 0000000000..2ad1adee92 --- /dev/null +++ b/resolvers/webpack/test/.eslintrc @@ -0,0 +1,5 @@ +--- +env: + mocha: true +rules: + quotes: 0 diff --git a/resolvers/webpack/test/alias.js b/resolvers/webpack/test/alias.js new file mode 100644 index 0000000000..8ea0e1b3dd --- /dev/null +++ b/resolvers/webpack/test/alias.js @@ -0,0 +1,75 @@ +var chai = require('chai') + , expect = chai.expect + , path = require('path') + +var webpack = require('../index') + +var file = path.join(__dirname, 'files', 'dummy.js') + +describe("resolve.alias", function () { + it("works", function () { + expect(webpack.resolveImport('foo', file)).to.exist + .and.equal(path.join(__dirname, 'files', 'some', 'goofy', 'path', 'foo.js')) + }) +}) + +var resolveAlias = require('../resolve-alias') +describe("webpack alias spec", function () { + // from table: http://webpack.github.io/docs/configuration.html#resolve-alias + function tableLine(alias, xyz, xyzFile) { + describe(JSON.stringify(alias), function () { + it("xyz: " + xyz, function () { + expect(resolveAlias('xyz', alias)).to.equal(xyz) + }) + it("xyz/file: " + (xyzFile.name || xyzFile), function () { + if (xyzFile === Error) { + expect(resolveAlias.bind(null, 'xyz/file', alias)).to.throw(xyzFile) + } else { + expect(resolveAlias('xyz/file', alias)).to.equal(xyzFile) + } + }) + }) + } + + tableLine( {} + , 'xyz', 'xyz/file' ) + + tableLine( { xyz: "/absolute/path/to/file.js" } + , '/absolute/path/to/file.js', 'xyz/file' ) + + tableLine( { xyz$: "/absolute/path/to/file.js" } + , "/absolute/path/to/file.js", Error ) + + tableLine( { xyz: "./dir/file.js" } + , './dir/file.js', 'xyz/file' ) + + tableLine( { xyz$: "./dir/file.js" } + , './dir/file.js', Error ) + + tableLine( { xyz: "/some/dir" } + , '/some/dir', '/some/dir/file' ) + + tableLine( { xyz$: "/some/dir" } + , '/some/dir', 'xyz/file' ) + + tableLine( { xyz: "./dir" } + , './dir', './dir/file' ) + + tableLine( { xyz: "modu" } + , 'modu', 'modu/file' ) + + tableLine( { xyz$: "modu" } + , 'modu', 'xyz/file' ) + + tableLine( { xyz: "modu/some/file.js" } + , 'modu/some/file.js', Error ) + + tableLine( { xyz: "modu/dir" } + , 'modu/dir', 'modu/dir/file' ) + + tableLine( { xyz: "xyz/dir" } + , 'xyz/dir', 'xyz/dir/file' ) + + tableLine( { xyz$: "xyz/dir" } + , 'xyz/dir', 'xyz/file' ) +}) diff --git a/resolvers/webpack/test/custom-extensions/bar.coffee b/resolvers/webpack/test/custom-extensions/bar.coffee new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resolvers/webpack/test/custom-extensions/baz.web.js b/resolvers/webpack/test/custom-extensions/baz.web.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resolvers/webpack/test/custom-extensions/foo.js b/resolvers/webpack/test/custom-extensions/foo.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resolvers/webpack/test/custom-extensions/package.json b/resolvers/webpack/test/custom-extensions/package.json new file mode 100644 index 0000000000..2528e52076 --- /dev/null +++ b/resolvers/webpack/test/custom-extensions/package.json @@ -0,0 +1 @@ +{ "dummy": true } diff --git a/resolvers/webpack/test/custom-extensions/webpack.config.js b/resolvers/webpack/test/custom-extensions/webpack.config.js new file mode 100644 index 0000000000..ee3444c00f --- /dev/null +++ b/resolvers/webpack/test/custom-extensions/webpack.config.js @@ -0,0 +1,3 @@ +module.exports = { + resolve: { extensions: ['.js', '.coffee'] }, +} diff --git a/resolvers/webpack/test/extensions.js b/resolvers/webpack/test/extensions.js new file mode 100644 index 0000000000..ab6fc84f48 --- /dev/null +++ b/resolvers/webpack/test/extensions.js @@ -0,0 +1,30 @@ +import { expect } from 'chai' +import { resolveImport as resolve } from '../index' + +import path from 'path' + +const file = path.join(__dirname, 'files', 'dummy.js') + , extensions = path.join(__dirname, 'custom-extensions', 'dummy.js') + +describe("extensions", () => { + it("respects the defaults", () => { + expect(resolve('./foo', file)).to.exist + .and.equal(path.join(__dirname, 'files', 'foo.web.js')) + }) + + describe("resolve.extensions set", () => { + it("works", () => { + expect(resolve('./foo', extensions)).to.exist + .and.equal(path.join(__dirname, 'custom-extensions', 'foo.js')) + }) + + it("replaces defaults", () => { + expect(() => resolve('./baz', extensions)).to.throw(Error) + }) + + it("finds .coffee", () => { + expect(resolve('./bar', extensions)).to.exist + .and.equal(path.join(__dirname, 'custom-extensions', 'bar.coffee')) + }) + }) +}) diff --git a/resolvers/webpack/test/externals.js b/resolvers/webpack/test/externals.js new file mode 100644 index 0000000000..baea7bdb62 --- /dev/null +++ b/resolvers/webpack/test/externals.js @@ -0,0 +1,21 @@ +var chai = require('chai') + , expect = chai.expect + , path = require('path') + +var webpack = require('../index') + +var file = path.join(__dirname, 'files', 'dummy.js') + +describe("externals", function () { + it("works on just a string", function () { + expect(webpack.resolveImport('bootstrap', file)).to.be.null + }) + + it("works on object-map", function () { + expect(webpack.resolveImport('jquery', file)).to.be.null + }) + + it("returns null for core modules", function () { + expect(webpack.resolveImport('fs', file)).to.be.null + }) +}) diff --git a/resolvers/webpack/test/files/bower_components/typeahead.js b/resolvers/webpack/test/files/bower_components/typeahead.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resolvers/webpack/test/files/foo.web.js b/resolvers/webpack/test/files/foo.web.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resolvers/webpack/test/files/node_modules/some-module/index.js b/resolvers/webpack/test/files/node_modules/some-module/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resolvers/webpack/test/files/package.json b/resolvers/webpack/test/files/package.json new file mode 100644 index 0000000000..d1d80ba6c1 --- /dev/null +++ b/resolvers/webpack/test/files/package.json @@ -0,0 +1 @@ +{ "just for show": true } diff --git a/resolvers/webpack/test/files/some/goofy/path/foo.js b/resolvers/webpack/test/files/some/goofy/path/foo.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resolvers/webpack/test/files/src/jsx/some-file.js b/resolvers/webpack/test/files/src/jsx/some-file.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resolvers/webpack/test/files/src/main-module.js b/resolvers/webpack/test/files/src/main-module.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resolvers/webpack/test/files/webpack.config.js b/resolvers/webpack/test/files/webpack.config.js new file mode 100644 index 0000000000..6cf72257f2 --- /dev/null +++ b/resolvers/webpack/test/files/webpack.config.js @@ -0,0 +1,16 @@ +var path = require('path') + +module.exports = { + resolve: { + alias: { + 'foo': path.join(__dirname, 'some', 'goofy', 'path', 'foo.js'), + }, + modulesDirectories: ['node_modules', 'bower_components'], + root: path.join(__dirname, 'src'), + }, + + externals: [ + { 'jquery': 'jQuery' }, + 'bootstrap', + ], +} diff --git a/resolvers/webpack/test/modules.js b/resolvers/webpack/test/modules.js new file mode 100644 index 0000000000..21f5721fae --- /dev/null +++ b/resolvers/webpack/test/modules.js @@ -0,0 +1,16 @@ +import { expect } from 'chai' +import { resolveImport as resolve } from '../index' +import path from 'path' + +const file = path.join(__dirname, 'files', 'dummy.js') + +describe("resolve.moduleDirectories", () => { + it("finds a node module", () => { + expect(resolve('some-module', file)).to.exist + .and.equal(path.join(__dirname, 'files', 'node_modules', 'some-module', 'index.js')) + }) + it("finds a bower module", () => { + expect(resolve('typeahead.js', file)).to.exist + .and.equal(path.join(__dirname, 'files', 'bower_components', 'typeahead.js')) + }) +}) diff --git a/resolvers/webpack/test/root.js b/resolvers/webpack/test/root.js new file mode 100644 index 0000000000..97e8b7e184 --- /dev/null +++ b/resolvers/webpack/test/root.js @@ -0,0 +1,19 @@ +var chai = require('chai') + , expect = chai.expect + +import { resolveImport as resolve } from '../index' + +import path from 'path' + +var file = path.join(__dirname, 'files', 'src', 'dummy.js') + +describe("root", function () { + it("works", function () { + expect(resolve('main-module', file)).to.exist + .and.equal(path.join(__dirname, 'files', 'src', 'main-module.js')) + }) + it("really works", function () { + expect(resolve('jsx/some-file', file)).to.exist + .and.equal(path.join(__dirname, 'files', 'src', 'jsx', 'some-file.js')) + }) +}) diff --git a/src/core/getExports.js b/src/core/getExports.js index 7d14b26598..87c1bad88d 100644 --- a/src/core/getExports.js +++ b/src/core/getExports.js @@ -4,7 +4,6 @@ import { createHash } from 'crypto' import parse from './parse' import resolve from './resolve' -import { isCore } from 'resolve' import isIgnored from './ignore' // map from settings sha1 => path => export map objects @@ -24,8 +23,6 @@ export default class ExportMap { get hasNamed() { return this.named.size > (this.hasDefault ? 1 : 0) } static get(source, context) { - // no use trying to parse core modules - if (isCore(source)) return null var path = resolve(source, context) if (path == null || isIgnored(path, context)) return null diff --git a/src/core/resolve.js b/src/core/resolve.js index a466c83a78..8c5a76c20d 100644 --- a/src/core/resolve.js +++ b/src/core/resolve.js @@ -1,18 +1,14 @@ -var fs = require('fs') - , path = require('path') - , resolve = require('resolve') +import fs from 'fs' +import { dirname, basename, join } from 'path' -const CASE_INSENSITIVE = fs.existsSync(path.join(__dirname, 'reSOLVE.js')) +const CASE_INSENSITIVE = fs.existsSync(join(__dirname, 'reSOLVE.js')) // http://stackoverflow.com/a/27382838 function fileExistsWithCaseSync(filepath) { - // shortcut exit - if (!fs.existsSync(filepath)) return false - - var dir = path.dirname(filepath) + var dir = dirname(filepath) if (dir === '/' || dir === '.' || /^[A-Z]:\\$/i.test(dir)) return true var filenames = fs.readdirSync(dir) - if (filenames.indexOf(path.basename(filepath)) === -1) { + if (filenames.indexOf(basename(filepath)) === -1) { return false } return fileExistsWithCaseSync(dir) @@ -20,70 +16,76 @@ function fileExistsWithCaseSync(filepath) { function fileExists(filepath) { if (CASE_INSENSITIVE) { - return fileExistsWithCaseSync(filepath) + // short-circuit if path doesn't exist, ignoring case + return !(!fs.existsSync(filepath) || !fileExistsWithCaseSync(filepath)) } else { return fs.existsSync(filepath) } } -function opts(basedir, settings) { - // pulls all items from 'import/resolve' - return Object.assign( { } - , settings && settings['import/resolve'] - , { basedir: basedir } - ) -} +export function relative(modulePath, sourceFile, settings) { -/** - * wrapper around resolve - * @param {string} p - module path - * @param {object} context - ESLint context - * @return {string} - the full module filesystem path - */ -module.exports = function (p, context) { - function withResolver(resolver) { + function withResolver(resolver, config) { // resolve just returns the core module id, which won't appear to exist - if (resolver.isCore(p)) return p - try { - var file = resolver.sync(p, opts( path.dirname(context.getFilename()) - , context.settings)) - if (!fileExists(file)) return null - return file - } catch (err) { + const filePath = resolver.resolveImport(modulePath, sourceFile, config) + if (filePath === null) return null - // probably want something more general here - if (err.message.indexOf('Cannot find module') === 0) { - return null - } + if (filePath === undefined || !fileExists(filePath)) return undefined - throw err + return filePath + } catch (err) { + return undefined } } - const resolvers = (context.settings['import/resolvers'] || ['resolve']) - .map(require) + const configResolvers = (settings['import/resolver'] + || { 'node': settings['import/resolve'] }) // backward compatibility + + const resolvers = resolverReducer(configResolvers, new Map()) + + for (let [name, config] of resolvers.entries()) { + const resolver = require(`eslint-import-resolver-${name}`) - for (let resolver of resolvers) { - let file = withResolver(resolver) - if (file) return file + let fullPath = withResolver(resolver, config) + if (fullPath !== undefined) return fullPath } - return null } -module.exports.relative = function (p, r, settings) { - try { - - var file = resolve.sync(p, opts(path.dirname(r), settings)) - if (!fileExists(file)) return null - return file +function resolverReducer(resolvers, map) { + if (resolvers instanceof Array) { + resolvers.forEach(r => resolverReducer(r, map)) + return map + } - } catch (err) { + if (typeof resolvers === 'string') { + map.set(resolvers, null) + return map + } - if (err.message.indexOf('Cannot find module') === 0) return null + if (typeof resolvers === 'object') { + for (let key in resolvers) { + map.set(key, resolvers[key]) + } + return map + } - throw err // else + throw new Error('invalid resolver config') +} - } +/** + * Givent + * @param {string} p - module path + * @param {object} context - ESLint context + * @return {string} - the full module filesystem path; + * null if package is core; + * undefined if not found + */ +export default function resolve(p, context) { + return relative( p + , context.getFilename() + , context.settings + ) } +resolve.relative = relative diff --git a/src/rules/no-unresolved.js b/src/rules/no-unresolved.js index a7af2ac075..402790c8c9 100644 --- a/src/rules/no-unresolved.js +++ b/src/rules/no-unresolved.js @@ -9,7 +9,7 @@ module.exports = function (context) { function checkSourceValue(source) { if (source == null) return - if (resolve(source.value, context) == null) { + if (resolve(source.value, context) === undefined) { context.report(source, 'Unable to resolve path to module \'' + source.value + '\'.') } diff --git a/tests/files/package.json b/tests/files/package.json new file mode 100644 index 0000000000..0a5fb6e237 --- /dev/null +++ b/tests/files/package.json @@ -0,0 +1 @@ +{ "dummy": true } \ No newline at end of file diff --git a/tests/files/webpack.config.js b/tests/files/webpack.config.js new file mode 100644 index 0000000000..93986f37a3 --- /dev/null +++ b/tests/files/webpack.config.js @@ -0,0 +1,6 @@ +module.exports = { + resolve: { + extensions: ['.js', '.jsx'], + root: __dirname, + }, +} diff --git a/tests/files/webpack.empty.config.js b/tests/files/webpack.empty.config.js new file mode 100644 index 0000000000..7c6d6c73d3 --- /dev/null +++ b/tests/files/webpack.empty.config.js @@ -0,0 +1 @@ +module.exports = {} \ No newline at end of file diff --git a/tests/src/core/resolve.js b/tests/src/core/resolve.js index 72c95c1d96..3e48a5d71d 100644 --- a/tests/src/core/resolve.js +++ b/tests/src/core/resolve.js @@ -1,9 +1,7 @@ -'use strict' +import { expect } from 'chai' +import resolve from 'core/resolve' -var expect = require('chai').expect - , resolve = require('../../../lib/core/resolve') - -var utils = require('../utils') +import * as utils from '../utils' describe('resolve', function () { it('should throw on bad parameters.', function () { @@ -24,6 +22,6 @@ describe('resolve', function () { , utils.testContext({ 'import/resolve': { 'extensions': ['.jsx'] }}) ) - expect(file).to.equal(null) + expect(file, 'path to ./jsx/MyUncoolComponent').to.be.undefined }) }) diff --git a/tests/src/rules/no-unresolved.js b/tests/src/rules/no-unresolved.js index c9cc0bd3fc..60bb8b7009 100644 --- a/tests/src/rules/no-unresolved.js +++ b/tests/src/rules/no-unresolved.js @@ -7,13 +7,182 @@ import { RuleTester } from 'eslint' var ruleTester = new RuleTester() , rule = require('../../../lib/rules/no-unresolved') -ruleTester.run('no-unresolved', rule, { +function runResolverTests(resolver) { + // redefine 'test' to set a resolver + // thus 'rest'. needed something 4-chars-long for formatting simplicity + function rest(specs) { + specs.settings = Object.assign({}, + specs.settings, + { 'import/resolver': resolver } + ) + + return test(specs) + } + + ruleTester.run(`no-unresolved (${resolver})`, rule, { + valid: [ + rest({ code: 'import foo from "./bar";' }), + rest({ code: "import bar from './bar.js';" }), + rest({ code: "import {someThing} from './module';" }), + rest({ code: "import fs from 'fs';" }), + + rest({ code: 'import * as foo from "a"' }), + + rest({ code: 'export { foo } from "./bar"' }), + rest({ code: 'export * from "./bar"' }), + rest({ code: 'export { foo }' }), + + // stage 1 proposal for export symmetry, + rest({ code: 'export * as bar from "./bar"' + , parser: 'babel-eslint' }), + rest({ code: 'export bar from "./bar"' + , parser: 'babel-eslint' }), + rest({ code: 'import foo from "./jsx/MyUnCoolComponent.jsx"' }), + + // commonjs setting + rest({ code: 'var foo = require("./bar")' + , options: [{ commonjs: true }]}), + rest({ code: 'require("./bar")' + , options: [{ commonjs: true }]}), + rest({ code: 'require("./does-not-exist")' + , options: [{ commonjs: false }]}), + rest({ code: 'require("./does-not-exist")' }), + + // amd setting + rest({ code: 'require(["./bar"], function (bar) {})' + , options: [{ amd: true }]}), + rest({ code: 'define(["./bar"], function (bar) {})' + , options: [{ amd: true }]}), + rest({ code: 'require(["./does-not-exist"], function (bar) {})' + , options: [{ amd: false }]}), + // don't validate without callback param + rest({ code: 'require(["./does-not-exist"])' + , options: [{ amd: true }]}), + rest({ code: 'define(["./does-not-exist"], function (bar) {})' }), + + // stress tests + rest({ code: 'require("./does-not-exist", "another arg")' + , options: [{ commonjs: true, amd: true }]}), + rest({ code: 'proxyquire("./does-not-exist")' + , options: [{ commonjs: true, amd: true }]}), + rest({ code: '(function() {})("./does-not-exist")' + , options: [{ commonjs: true, amd: true }]}), + rest({ code: 'define([0, foo], function (bar) {})' + , options: [{ amd: true }]}), + rest({ code: 'require(0)' + , options: [{ commonjs: true }]}), + rest({ code: 'require(foo)' + , options: [{ commonjs: true }]}), + + ], + + invalid: [ + rest({ + code: 'import reallyfake from "./reallyfake/module"', + settings: { 'import/ignore': ['^\\./fake/'] }, + errors: [{ message: 'Unable to resolve path to module ' + + '\'./reallyfake/module\'.' }], + }), + + + rest({ + code: "import bar from './baz';", + errors: [{ message: "Unable to resolve path to module './baz'." + , type: 'Literal' }], + }), + rest({ code: "import bar from './baz';" + , errors: [{ message: "Unable to resolve path to module './baz'." + , type: 'Literal', + }] }), + rest({ + code: "import bar from './empty-folder';", + errors: [{ message: "Unable to resolve path to module './empty-folder'." + , type: 'Literal', + }]}), + + // sanity check that this module is _not_ found without proper settings + rest({ + code: "import { DEEP } from 'in-alternate-root';", + errors: [{ message: 'Unable to resolve path to ' + + "module 'in-alternate-root'." + , type: 'Literal', + }]}), + + rest({ code: 'export { foo } from "./does-not-exist"' + , errors: 1 }), + rest({ + code: 'export * from "./does-not-exist"', + errors: 1, + }), + + // export symmetry proposal + rest({ code: 'export * as bar from "./does-not-exist"' + , parser: 'babel-eslint' + , errors: 1, + }), + rest({ code: 'export bar from "./does-not-exist"' + , parser: 'babel-eslint' + , errors: 1, + }), + + rest({ code: 'import foo from "./jsx/MyUncoolComponent.jsx"' + , errors: 1 }), + + + // commonjs setting + rest({ + code: 'var bar = require("./baz")', + options: [{ commonjs: true }], + errors: [{ + message: "Unable to resolve path to module './baz'.", + type: 'Literal', + }], + }), + rest({ + code: 'require("./baz")', + options: [{ commonjs: true }], + errors: [{ + message: "Unable to resolve path to module './baz'.", + type: 'Literal', + }], + }), + + // amd + rest({ + code: 'require(["./baz"], function (bar) {})', + options: [{ amd: true }], + errors: [{ + message: "Unable to resolve path to module './baz'.", + type: 'Literal', + }], + }), + rest({ + code: 'define(["./baz"], function (bar) {})', + options: [{ amd: true }], + errors: [{ + message: "Unable to resolve path to module './baz'.", + type: 'Literal', + }], + }), + rest({ + code: 'define(["./baz", "./bar", "./does-not-exist"], function (bar) {})', + options: [{ amd: true }], + errors: [{ + message: "Unable to resolve path to module './baz'.", + type: 'Literal', + },{ + message: "Unable to resolve path to module './does-not-exist'.", + type: 'Literal', + }], + }), + ], + }) +} + +['node', 'webpack'].forEach(runResolverTests) + +ruleTester.run('no-unresolved (import/resolve legacy)', rule, { valid: [ - test({ code: 'import foo from "./bar";' }), - test({ code: "import bar from './bar.js';" }), - test({ code: "import {someThing} from './module';" }), - test({ code: "import fs from 'fs';" }), - test({ code: "import { DEEP } from 'in-alternate-root';", settings: { @@ -23,6 +192,7 @@ ruleTester.run('no-unresolved', rule, { }, }, }), + test({ code: "import { DEEP } from 'in-alternate-root'; " + "import { bar } from 'src-bar';", @@ -31,167 +201,41 @@ ruleTester.run('no-unresolved', rule, { path.join('tests', 'files', 'alternate-root'), ]}}}), - test({ code: 'import * as foo from "a"' }), - test({ code: 'import * as foo from "jsx-module/foo"', settings: { 'import/resolve': { 'extensions': ['.jsx'] } }, }), - - test({ code: 'export { foo } from "./bar"' }), - test({ code: 'export * from "./bar"' }), - test({ code: 'export { foo }' }), - - // stage 1 proposal for export symmetry, - test({ code: 'export * as bar from "./bar"' - , parser: 'babel-eslint' }), - test({ code: 'export bar from "./bar"' - , parser: 'babel-eslint' }), - test({ code: 'import foo from "./jsx/MyUnCoolComponent.jsx"' }), - - // commonjs setting - test({ code: 'var foo = require("./bar")' - , options: [{ commonjs: true }]}), - test({ code: 'require("./bar")' - , options: [{ commonjs: true }]}), - test({ code: 'require("./does-not-exist")' - , options: [{ commonjs: false }]}), - test({ code: 'require("./does-not-exist")' }), - - // amd setting - test({ code: 'require(["./bar"], function (bar) {})' - , options: [{ amd: true }]}), - test({ code: 'define(["./bar"], function (bar) {})' - , options: [{ amd: true }]}), - test({ code: 'require(["./does-not-exist"], function (bar) {})' - , options: [{ amd: false }]}), - // don't validate without callback param - test({ code: 'require(["./does-not-exist"])' - , options: [{ amd: true }]}), - test({ code: 'define(["./does-not-exist"], function (bar) {})' }), - - // stress tests - test({ code: 'require("./does-not-exist", "another arg")' - , options: [{ commonjs: true, amd: true }]}), - test({ code: 'proxyquire("./does-not-exist")' - , options: [{ commonjs: true, amd: true }]}), - test({ code: '(function() {})("./does-not-exist")' - , options: [{ commonjs: true, amd: true }]}), - test({ code: 'define([0, foo], function (bar) {})' - , options: [{ amd: true }]}), - test({ code: 'require(0)' - , options: [{ commonjs: true }]}), - test({ code: 'require(foo)' - , options: [{ commonjs: true }]}), - ], invalid: [ - // should fail for jsx by default test({ code: 'import * as foo from "jsx-module/foo"', - errors: [ {message: 'Unable to resolve path to ' + - 'module \'jsx-module/foo\'.'} ], - }), - - - test({ - code: 'import reallyfake from "./reallyfake/module"', - settings: { 'import/ignore': ['^\\./fake/'] }, - errors: [{ message: 'Unable to resolve path to module ' + - '\'./reallyfake/module\'.' }], - }), - - - test({ - code: "import bar from './baz';", - errors: [{ message: "Unable to resolve path to module './baz'." - , type: 'Literal' }], - }), - test({ code: "import bar from './baz';" - , errors: [{ message: "Unable to resolve path to module './baz'." - , type: 'Literal', - }] }), - test({ - code: "import bar from './empty-folder';", - errors: [{ message: "Unable to resolve path to module './empty-folder'." - , type: 'Literal', - }]}), - - // sanity check that this module is _not_ found without proper settings - test({ - code: "import { DEEP } from 'in-alternate-root';", - errors: [{ message: 'Unable to resolve path to ' + - "module 'in-alternate-root'." - , type: 'Literal', - }]}), - - test({ code: 'export { foo } from "./does-not-exist"' - , errors: 1 }), - test({ - code: 'export * from "./does-not-exist"', - errors: 1, - }), - - // export symmetry proposal - test({ code: 'export * as bar from "./does-not-exist"' - , parser: 'babel-eslint' - , errors: 1, - }), - test({ code: 'export bar from "./does-not-exist"' - , parser: 'babel-eslint' - , errors: 1, - }), - - test({ code: 'import foo from "./jsx/MyUncoolComponent.jsx"' - , errors: 1 }), - - - // commonjs setting - test({ - code: 'var bar = require("./baz")', - options: [{ commonjs: true }], - errors: [{ - message: "Unable to resolve path to module './baz'.", - type: 'Literal', - }], - }), - test({ - code: 'require("./baz")', - options: [{ commonjs: true }], - errors: [{ - message: "Unable to resolve path to module './baz'.", - type: 'Literal', - }], + errors: [ "Unable to resolve path to module 'jsx-module/foo'." ], }), + ], +}) - // amd +ruleTester.run('no-unresolved (webpack-specific)', rule, { + valid: [ test({ - code: 'require(["./baz"], function (bar) {})', - options: [{ amd: true }], - errors: [{ - message: "Unable to resolve path to module './baz'.", - type: 'Literal', - }], + // default webpack config in files/webpack.config.js knows about jsx + code: 'import * as foo from "jsx-module/foo"', + settings: { 'import/resolver': 'webpack' }, }), test({ - code: 'define(["./baz"], function (bar) {})', - options: [{ amd: true }], - errors: [{ - message: "Unable to resolve path to module './baz'.", - type: 'Literal', - }], + // should ignore loaders + code: 'import * as foo from "some-loader?with=args!jsx-module/foo"', + settings: { 'import/resolver': 'webpack' }, }), + ], + invalid: [ test({ - code: 'define(["./baz", "./bar", "./does-not-exist"], function (bar) {})', - options: [{ amd: true }], - errors: [{ - message: "Unable to resolve path to module './baz'.", - type: 'Literal', - },{ - message: "Unable to resolve path to module './does-not-exist'.", - type: 'Literal', - }], + // default webpack config in files/webpack.config.js knows about jsx + code: 'import * as foo from "jsx-module/foo"', + settings: { + 'import/resolver': { 'webpack': { 'config': 'webpack.empty.config.js' } }, + }, + errors: [ "Unable to resolve path to module 'jsx-module/foo'." ], }), ], })