diff --git a/CHANGELOG.md b/CHANGELOG.md index b68d8b4e04..7fc2526f58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,16 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com). -## resolvers/webpack/0.2.5 - 2016-05-23 +## [Unreleased] + + +## [1.9.0] - 2016-06-10 ### Added -- Added support for multiple webpack configs ([#181], thanks [@GreenGremlin]) +- Added support TomDoc comments to [`no-deprecated`]. ([#321], thanks [@josh]) +- Added support for loading custom resolvers ([#314], thanks [@le0nik]) + +### Fixed +- [`prefer-default-export`] handles `export function` and `export const` in same file ([#359], thanks [@scottnonnenberg]) ## [1.8.1] - 2016-05-23 ### Fixed @@ -38,10 +45,6 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel - [`extensions`]: fallback to source path for extension enforcement if imported module is not resolved. Also, never report for builtins (i.e. `path`). ([#296]) -## resolvers/webpack/0.2.4 - 2016-04-29 -### Changed -- automatically find webpack config with `interpret`-able extensions ([#287], thanks [@taion]) - ## [1.6.1] - 2016-04-28 ### Fixed - [`no-named-as-default-member`]: don't crash on rest props. ([#281], thanks [@SimenB]) @@ -49,16 +52,6 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel Thanks to [@strawbrary] for bringing this up ([#272]) and adding OSX support to the Travis config ([#288]). -## resolvers/webpack/0.2.3 - 2016-04-28 -### Fixed -- `interpret` dependency was declared in the wrong `package.json`. - Thanks [@jonboiser] for sleuthing ([#286]) and fixing ([#289]). - -## resolvers/webpack/0.2.2 - 2016-04-27 -### Added -- `interpret` configs (such as `.babel.js`). - Thanks to [@gausie] for the initial PR ([#164], ages ago! 😅) and [@jquense] for tests ([#278]). - ## [1.6.0] - 2016-04-25 ### Added - add [`no-named-as-default-member`] to `warnings` canned config @@ -225,9 +218,11 @@ for info on changes for earlier releases. [`no-mutable-exports`]: ./docs/rules/no-mutable-exports.md [`prefer-default-export`]: ./docs/rules/prefer-default-export.md +[#359]: https://github.com/benmosher/eslint-plugin-import/pull/359 [#343]: https://github.com/benmosher/eslint-plugin-import/pull/343 [#332]: https://github.com/benmosher/eslint-plugin-import/pull/332 [#322]: https://github.com/benmosher/eslint-plugin-import/pull/322 +[#321]: https://github.com/benmosher/eslint-plugin-import/pull/321 [#316]: https://github.com/benmosher/eslint-plugin-import/pull/316 [#308]: https://github.com/benmosher/eslint-plugin-import/pull/308 [#298]: https://github.com/benmosher/eslint-plugin-import/pull/298 @@ -251,6 +246,7 @@ for info on changes for earlier releases. [#211]: https://github.com/benmosher/eslint-plugin-import/pull/211 [#164]: https://github.com/benmosher/eslint-plugin-import/pull/164 [#157]: https://github.com/benmosher/eslint-plugin-import/pull/157 +[#314]: https://github.com/benmosher/eslint-plugin-import/pull/314 [#342]: https://github.com/benmosher/eslint-plugin-import/issues/342 [#328]: https://github.com/benmosher/eslint-plugin-import/issues/328 @@ -272,7 +268,8 @@ for info on changes for earlier releases. [#119]: https://github.com/benmosher/eslint-plugin-import/issues/119 [#89]: https://github.com/benmosher/eslint-plugin-import/issues/89 -[Unreleased]: https://github.com/benmosher/eslint-plugin-import/compare/v1.8.1...HEAD +[Unreleased]: https://github.com/benmosher/eslint-plugin-import/compare/v1.9.0...HEAD +[1.9.0]: https://github.com/benmosher/eslint-plugin-import/compare/v1.8.1...v1.9.0 [1.8.1]: https://github.com/benmosher/eslint-plugin-import/compare/v1.8.0...v1.8.1 [1.8.0]: https://github.com/benmosher/eslint-plugin-import/compare/v1.7.0...v1.8.0 [1.7.0]: https://github.com/benmosher/eslint-plugin-import/compare/v1.6.1...v1.7.0 @@ -312,4 +309,5 @@ for info on changes for earlier releases. [@borisyankov]: https://github.com/borisyankov [@gavriguy]: https://github.com/gavriguy [@jkimbo]: https://github.com/jkimbo +[@le0nik]: https://github.com/le0nik [@scottnonnenberg]: https://github.com/scottnonnenberg diff --git a/README.md b/README.md index 37bdd861e7..0181a4275a 100644 --- a/README.md +++ b/README.md @@ -123,19 +123,79 @@ In the interest of supporting both of these, v0.11 introduces resolvers. Currently [Node] and [Webpack] resolution have been implemented, but the resolvers are just npm packages, so [third party packages are supported](https://github.com/benmosher/eslint-plugin-import/wiki/Resolvers) (and encouraged!). -Just install a resolver as `eslint-import-resolver-foo` and reference it as such: +You can reference resolvers in several ways(in order of precedence): + +- with an absolute path to resolver, used as a `computed property` name, which is supported since Node v4: + +```js +// .eslintrc.js +module.exports = { + settings: { + 'import/resolver': { + [path.resolve('../../../my-resolver')]: { someConfig: value } + } + } +} +``` + +- with a path relative to the closest `package.json` file: + +```js +// .eslintrc.js +module.exports = { + settings: { + 'import/resolver': { + './my-resolver': { someConfig: value } + } + } +} +``` + ```yaml +# .eslintrc.yml settings: - import/resolver: foo + import/resolver: './my-resolver' +``` + +- with an npm module name, like `my-awesome-npm-module`: + +```js +// .eslintrc.js +module.exports = { + settings: { + 'import/resolver': { + 'my-awesome-npm-module': { someConfig: value } + } + } +} ``` -or with a config object: ```yaml +# .eslintrc.yml settings: - import/resolver: - foo: { someConfigKey: value } + import/resolver: 'my-awesome-npm-module' +``` + +- as a conventional `eslint-import-resolver` name, like `eslint-import-resolver-foo`: + + +```js +// .eslintrc.js +module.exports = { + settings: { + 'import/resolver': { + foo: { someConfig: value } + } + } +} +``` + +`.eslintrc.yml`: +```yaml +settings: + import/resolver: foo ``` If you are interesting in writing a resolver, see the [spec](./resolvers/README.md) for more details. diff --git a/docs/rules/no-deprecated.md b/docs/rules/no-deprecated.md index 7f73eab780..989534ac6d 100644 --- a/docs/rules/no-deprecated.md +++ b/docs/rules/no-deprecated.md @@ -5,7 +5,9 @@ **NOTE**: this rule is currently a work in progress. There may be "breaking" changes: most likely, additional cases that are flagged. Reports use of a deprecated name, as indicated by a JSDoc block with a `@deprecated` -tag, i.e. +tag or TomDoc `Deprecated: ` comment. + +using a JSDoc `@deprecated` tag: ```js // @file: ./answer.js @@ -30,6 +32,28 @@ function whatever(y, z) { } ``` +or using the TomDoc equivalent: + +```js +// Deprecated: This is what you get when you trust a mouse talk show, need to +// restart the experiment. +// +// Returns a Number nonsense +export function multiply(six, nine) { + return 42 +} +``` + +Only JSDoc is enabled by default. Other documentation styles can be enabled with +the `import/docstyle` setting. + + +```yaml +# .eslintrc.yml +settings: + import/docstyle: ['jsdoc', 'tomdoc'] +``` + ### Worklist - [x] report explicit imports on the import node @@ -39,4 +63,3 @@ function whatever(y, z) { - [x] mark module deprecated if file JSDoc has a @deprecated tag? - [ ] don't flag redeclaration of imported, deprecated names - [ ] flag destructuring - diff --git a/package.json b/package.json index 4fa0b4f666..6eb459d0d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-import", - "version": "1.8.1", + "version": "1.9.0", "description": "Import with sanity.", "main": "lib/index.js", "directories": { @@ -71,11 +71,13 @@ "es6-set": "^0.1.4", "es6-symbol": "*", "eslint-import-resolver-node": "^0.2.0", + "is-absolute": "^0.2.5", "lodash.cond": "^4.3.0", "lodash.endswith": "^4.0.1", "lodash.find": "^4.3.0", "lodash.findindex": "^4.3.0", "object-assign": "^4.0.1", + "pkg-dir": "^1.0.0", "pkg-up": "^1.0.0" } } diff --git a/resolvers/webpack/CHANGELOG.md b/resolvers/webpack/CHANGELOG.md new file mode 100644 index 0000000000..657ed5f9a2 --- /dev/null +++ b/resolvers/webpack/CHANGELOG.md @@ -0,0 +1,53 @@ +# Change Log +All notable changes to this resolver will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). +This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com). + +## Unreleased +### Fixed +- strip resource query ([#357], thanks [@daltones]) +- allow `externals` to be defined as a function ([#363], thanks [@kesne]) + +## 0.3.1 - 2016-06-02 +### Added +- debug logging. run with `DEBUG=eslint-plugin-import:*` to see log output. + +## 0.3.0 - 2016-06-01 +### Changed +- use `enhanced-resolve` to support additional plugins instead of re-implementing + aliases, etc. + +## 0.2.5 - 2016-05-23 +### Added +- Added support for multiple webpack configs ([#181], thanks [@GreenGremlin]) + +## 0.2.4 - 2016-04-29 +### Changed +- automatically find webpack config with `interpret`-able extensions ([#287], thanks [@taion]) + +## 0.2.3 - 2016-04-28 +### Fixed +- `interpret` dependency was declared in the wrong `package.json`. + Thanks [@jonboiser] for sleuthing ([#286]) and fixing ([#289]). + +## 0.2.2 - 2016-04-27 +### Added +- `interpret` configs (such as `.babel.js`). + Thanks to [@gausie] for the initial PR ([#164], ages ago! 😅) and [@jquense] for tests ([#278]). + +[#363]: https://github.com/benmosher/eslint-plugin-import/pull/363 +[#289]: https://github.com/benmosher/eslint-plugin-import/pull/289 +[#287]: https://github.com/benmosher/eslint-plugin-import/pull/287 +[#278]: https://github.com/benmosher/eslint-plugin-import/pull/278 +[#181]: https://github.com/benmosher/eslint-plugin-import/pull/181 +[#164]: https://github.com/benmosher/eslint-plugin-import/pull/164 + +[#357]: https://github.com/benmosher/eslint-plugin-import/issues/357 +[#286]: https://github.com/benmosher/eslint-plugin-import/issues/286 + +[@gausie]: https://github.com/gausie +[@jquense]: https://github.com/jquense +[@taion]: https://github.com/taion +[@GreenGremlin]: https://github.com/GreenGremlin +[@daltones]: https://github.com/daltones +[@kesne]: https://github.com/kesne diff --git a/resolvers/webpack/index.js b/resolvers/webpack/index.js index 8919ef3884..2e2d8becd4 100644 --- a/resolvers/webpack/index.js +++ b/resolvers/webpack/index.js @@ -1,14 +1,14 @@ var findRoot = require('find-root') , path = require('path') - , resolve = require('resolve') , get = require('lodash.get') , find = require('array-find') , interpret = require('interpret') // not available on 0.10.x , isAbsolute = path.isAbsolute || require('is-absolute') , fs = require('fs') + , coreLibs = require('node-libs-browser') -var resolveAlias = require('./resolve-alias') +const log = require('debug')('eslint-plugin-import:resolver:webpack') exports.interfaceVersion = 2 @@ -30,19 +30,24 @@ exports.resolve = function (source, file, settings) { source = source.slice(finalBang + 1) } - if (resolve.isCore(source)) return { found: true, path: null } + // strip resource query + var finalQuestionMark = source.lastIndexOf('?') + if (finalQuestionMark >= 0) { + source = source.slice(0, finalQuestionMark) + } - var webpackConfig + if (source in coreLibs) { + return { found: true, path: coreLibs[source] } + } - var extensions = Object.keys(interpret.extensions).sort(function(a, b) { - return a === '.js' ? -1 : b === '.js' ? 1 : a.length - b.length - }) + var webpackConfig try { var configPath = get(settings, 'config') , configIndex = get(settings, 'config-index') , packageDir - , extension + + if (configPath) log('Config path from settings:', configPath) // see if we've got an absolute path if (!configPath || !isAbsolute(configPath)) { @@ -51,46 +56,17 @@ exports.resolve = function (source, file, settings) { if (!packageDir) throw new Error('package not found above ' + file) } - if (configPath) { - // extensions is not reused below, so safe to mutate it here. - extensions.reverse() - extensions.forEach(function (maybeExtension) { - if (extension) { - return - } + configPath = findConfigPath(configPath, packageDir) - if (configPath.substr(-maybeExtension.length) === maybeExtension) { - extension = maybeExtension - } - }) - - // see if we've got an absolute path - if (!isAbsolute(configPath)) { - configPath = path.join(packageDir, configPath) - } - } else { - extensions.forEach(function (maybeExtension) { - if (extension) { - return - } - - var maybePath = path.resolve( - path.join(packageDir, 'webpack.config' + maybeExtension) - ) - if (fs.existsSync(maybePath)) { - configPath = maybePath - extension = maybeExtension - } - }) - } - - registerCompiler(interpret.extensions[extension]) + log('Config path resolved to:', configPath) webpackConfig = require(configPath) if (webpackConfig && webpackConfig.default) { + log('Using ES6 module "default" key instead of module.exports.') webpackConfig = webpackConfig.default } } catch (err) { + log('Error during config lookup:', err) webpackConfig = {} } @@ -105,52 +81,79 @@ exports.resolve = function (source, file, settings) { } } + log('Using config: ', webpackConfig) + // externals if (findExternal(source, webpackConfig.externals)) return { found: true, path: 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) { - if (typeof rootPath === 'string') paths.push(rootPath) - else paths.push.apply(paths, rootPath) - } - - // set fallback paths - var fallbackPath = get(webpackConfig, ['resolve', 'fallback']) - if (fallbackPath) { - if (typeof fallbackPath === 'string') paths.push(fallbackPath) - else paths.push.apply(paths, fallbackPath) - } - // otherwise, resolve "normally" + var resolver = createResolver(webpackConfig.resolve || {}) try { - - return { found: true, path: 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, - packageFilter: packageFilter.bind(null, webpackConfig), - }) } + return { found: true, path: resolver.resolveSync(path.dirname(file), source) } } catch (err) { + log('Error during module resolution:', err) return { found: false } } } +var Resolver = require('enhanced-resolve/lib/Resolver') + +var SyncNodeJsInputFileSystem = require('enhanced-resolve/lib/SyncNodeJsInputFileSystem') +var syncFS = new SyncNodeJsInputFileSystem() + +var ModuleAliasPlugin = require('enhanced-resolve/lib/ModuleAliasPlugin') +var ModulesInDirectoriesPlugin = require('enhanced-resolve/lib/ModulesInDirectoriesPlugin') +var ModulesInRootPlugin = require('enhanced-resolve/lib/ModulesInRootPlugin') +var ModuleAsFilePlugin = require('enhanced-resolve/lib/ModuleAsFilePlugin') +var ModuleAsDirectoryPlugin = require('enhanced-resolve/lib/ModuleAsDirectoryPlugin') +var DirectoryDescriptionFilePlugin = require('enhanced-resolve/lib/DirectoryDescriptionFilePlugin') +var DirectoryDefaultFilePlugin = require('enhanced-resolve/lib/DirectoryDefaultFilePlugin') +var FileAppendPlugin = require('enhanced-resolve/lib/FileAppendPlugin') +var ResultSymlinkPlugin = require('enhanced-resolve/lib/ResultSymlinkPlugin') +var DirectoryDescriptionFileFieldAliasPlugin = + require('enhanced-resolve/lib/DirectoryDescriptionFileFieldAliasPlugin') + +// adapted from tests & +// https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L322 +function createResolver(resolve) { + var resolver = new Resolver(syncFS) + + resolver.apply( + resolve.packageAlias + ? new DirectoryDescriptionFileFieldAliasPlugin('package.json', resolve.packageAlias) + : function() {}, + new ModuleAliasPlugin(resolve.alias || {}), + makeRootPlugin('module', resolve.root), + new ModulesInDirectoriesPlugin('module', resolve.modulesDirectories || ['web_modules', 'node_modules']), + makeRootPlugin('module', resolve.fallback), + new ModuleAsFilePlugin('module'), + new ModuleAsDirectoryPlugin('module'), + new DirectoryDescriptionFilePlugin('package.json', ['jsnext:main'].concat(resolve.packageMains || defaultMains)), + new DirectoryDefaultFilePlugin(['index']), + new FileAppendPlugin(resolve.extensions || ['', '.webpack.js', '.web.js', '.js']), + new ResultSymlinkPlugin() + ) + + return resolver +} + +/* eslint-disable */ +// from https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L365 +function makeRootPlugin(name, root) { + if(typeof root === "string") + return new ModulesInRootPlugin(name, root); + else if(Array.isArray(root)) { + return function() { + root.forEach(function(root) { + this.apply(new ModulesInRootPlugin(name, root)); + }, this); + }; + } + return function() {}; +} +/* eslint-enable */ + function findExternal(source, externals) { if (!externals) return false @@ -167,7 +170,15 @@ function findExternal(source, externals) { } if (typeof externals === 'function') { - throw new Error('unable to handle function externals') + var functionExternalFound = false + externals.call(null, null, source, function(err, value) { + if (err) { + functionExternalFound = false + } else { + functionExternalFound = findExternal(source, value) + } + }) + return functionExternalFound } // else, vanilla object @@ -186,28 +197,50 @@ var defaultMains = [ 'webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main', ] -function packageFilter(config, pkg) { - var altMain +function findConfigPath(configPath, packageDir) { + var extensions = Object.keys(interpret.extensions).sort(function(a, b) { + return a === '.js' ? -1 : b === '.js' ? 1 : a.length - b.length + }) + , extension + - // check for rollup-style first - if (pkg['jsnext:main']) { - pkg['main'] = pkg['jsnext:main'] - } else { - // check for configured/default alternative main fields - altMain = find( - get(config, ['resolve', 'packageMains']) || defaultMains, - function (m) { return typeof get(pkg, m) === 'string' }) + if (configPath) { + // extensions is not reused below, so safe to mutate it here. + extensions.reverse() + extensions.forEach(function (maybeExtension) { + if (extension) { + return + } - if (altMain) { - pkg['main'] = get(pkg, altMain) + if (configPath.substr(-maybeExtension.length) === maybeExtension) { + extension = maybeExtension + } + }) + + // see if we've got an absolute path + if (!isAbsolute(configPath)) { + configPath = path.join(packageDir, configPath) } - } + } else { + extensions.forEach(function (maybeExtension) { + if (extension) { + return + } + var maybePath = path.resolve( + path.join(packageDir, 'webpack.config' + maybeExtension) + ) + if (fs.existsSync(maybePath)) { + configPath = maybePath + extension = maybeExtension + } + }) + } - return pkg + registerCompiler(interpret.extensions[extension]) + return configPath } - function registerCompiler(moduleDescriptor) { if(moduleDescriptor) { if(typeof moduleDescriptor === 'string') { @@ -220,7 +253,7 @@ function registerCompiler(moduleDescriptor) { registerCompiler(moduleDescriptor[i]) break } catch(e) { - // do nothing + log('Failed to register compiler for moduleDescriptor[]:', i, moduleDescriptor) } } } diff --git a/resolvers/webpack/package.json b/resolvers/webpack/package.json index 40bd4976f8..c97ef339df 100644 --- a/resolvers/webpack/package.json +++ b/resolvers/webpack/package.json @@ -1,6 +1,6 @@ { "name": "eslint-import-resolver-webpack", - "version": "0.2.5", + "version": "0.3.1", "description": "Resolve paths to dependencies, given a webpack.config.js. Plugin for eslint-plugin-import.", "main": "index.js", "scripts": { @@ -25,11 +25,13 @@ "homepage": "https://github.com/benmosher/eslint-plugin-import#readme", "dependencies": { "array-find": "^1.0.0", + "debug": "^2.2.0", + "enhanced-resolve": "~0.9.0", "find-root": "^0.1.1", "interpret": "^1.0.0", "is-absolute": "^0.2.3", "lodash.get": "^3.7.0", - "resolve": "^1.1.6" + "node-libs-browser": "^1.0.0" }, "peerDependencies": { "eslint-plugin-import": ">=1.4.0" diff --git a/resolvers/webpack/resolve-alias.js b/resolvers/webpack/resolve-alias.js deleted file mode 100644 index 485b41eecf..0000000000 --- a/resolvers/webpack/resolve-alias.js +++ /dev/null @@ -1,48 +0,0 @@ -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 -} - -// using '/' only for consistency with Webpack docs -var sep = '/' -function matchAlias(source, alias, value) { - var isExact = (alias[alias.length - 1] === '$') - , isFile = (path.extname(value) !== '') - , sourceSegments = source.split(sep) - , ptr = 0 - , aliasSegments - - if (isExact) alias = alias.slice(0, -1) - - aliasSegments = alias.split(sep) - - // look for a common prefix - while(sourceSegments[ptr] == aliasSegments[ptr] && ptr < sourceSegments.length && ptr < aliasSegments.length) { - ptr++; - } - - // the common prefix must be the entirety of the alias - if (ptr === aliasSegments.length) { - // always return exact match - if (sourceSegments.length === aliasSegments.length) 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(sourceSegments.slice(ptr)).join(sep) - } - -} diff --git a/resolvers/webpack/test/alias.js b/resolvers/webpack/test/alias.js index 4a120f05de..f8cd210e42 100644 --- a/resolvers/webpack/test/alias.js +++ b/resolvers/webpack/test/alias.js @@ -18,120 +18,120 @@ describe("resolve.alias", function () { }) }) -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) - } - }) - }) - } +// todo: reimplement with resolver function / config +// describe.skip("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' ) +// }) + +// describe.skip("nested module names", function () { +// // from table: http://webpack.github.io/docs/configuration.html#resolve-alias +// function nestedName(alias, xyz, xyzFile) { +// describe(JSON.stringify(alias), function () { +// it("top/xyz: " + xyz, function () { +// expect(resolveAlias('top/xyz', alias)).to.equal(xyz) +// }) +// it("top/xyz/file: " + (xyzFile.name || xyzFile), function () { +// if (xyzFile === Error) { +// expect(resolveAlias.bind(null, 'top/xyz/file', alias)).to.throw(xyzFile) +// } else { +// expect(resolveAlias('top/xyz/file', alias)).to.equal(xyzFile) +// } +// }) +// }) +// } + +// nestedName( { 'top/xyz': "/absolute/path/to/file.js" } +// , '/absolute/path/to/file.js', 'top/xyz/file' ) + +// nestedName( { 'top/xyz$': "/absolute/path/to/file.js" } +// , "/absolute/path/to/file.js", Error ) + +// nestedName( { 'top/xyz': "./dir/file.js" } +// , './dir/file.js', 'top/xyz/file' ) + +// nestedName( { 'top/xyz$': "./dir/file.js" } +// , './dir/file.js', Error ) + +// nestedName( { 'top/xyz': "/some/dir" } +// , '/some/dir', '/some/dir/file' ) + +// nestedName( { 'top/xyz$': "/some/dir" } +// , '/some/dir', 'top/xyz/file' ) + +// nestedName( { 'top/xyz': "./dir" } +// , './dir', './dir/file' ) + +// nestedName( { 'top/xyz': "modu" } +// , 'modu', 'modu/file' ) + +// nestedName( { 'top/xyz$': "modu" } +// , 'modu', 'top/xyz/file' ) - tableLine( {} - , 'xyz', 'xyz/file' ) +// nestedName( { 'top/xyz': "modu/some/file.js" } +// , 'modu/some/file.js', Error ) - tableLine( { xyz: "/absolute/path/to/file.js" } - , '/absolute/path/to/file.js', 'xyz/file' ) +// nestedName( { 'top/xyz': "modu/dir" } +// , 'modu/dir', 'modu/dir/file' ) - tableLine( { xyz$: "/absolute/path/to/file.js" } - , "/absolute/path/to/file.js", Error ) +// nestedName( { 'top/xyz': "top/xyz/dir" } +// , 'top/xyz/dir', 'top/xyz/dir/file' ) - 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' ) -}) - -describe("nested module names", function () { - // from table: http://webpack.github.io/docs/configuration.html#resolve-alias - function nestedName(alias, xyz, xyzFile) { - describe(JSON.stringify(alias), function () { - it("top/xyz: " + xyz, function () { - expect(resolveAlias('top/xyz', alias)).to.equal(xyz) - }) - it("top/xyz/file: " + (xyzFile.name || xyzFile), function () { - if (xyzFile === Error) { - expect(resolveAlias.bind(null, 'top/xyz/file', alias)).to.throw(xyzFile) - } else { - expect(resolveAlias('top/xyz/file', alias)).to.equal(xyzFile) - } - }) - }) - } - - nestedName( { 'top/xyz': "/absolute/path/to/file.js" } - , '/absolute/path/to/file.js', 'top/xyz/file' ) - - nestedName( { 'top/xyz$': "/absolute/path/to/file.js" } - , "/absolute/path/to/file.js", Error ) - - nestedName( { 'top/xyz': "./dir/file.js" } - , './dir/file.js', 'top/xyz/file' ) - - nestedName( { 'top/xyz$': "./dir/file.js" } - , './dir/file.js', Error ) - - nestedName( { 'top/xyz': "/some/dir" } - , '/some/dir', '/some/dir/file' ) - - nestedName( { 'top/xyz$': "/some/dir" } - , '/some/dir', 'top/xyz/file' ) - - nestedName( { 'top/xyz': "./dir" } - , './dir', './dir/file' ) - - nestedName( { 'top/xyz': "modu" } - , 'modu', 'modu/file' ) - - nestedName( { 'top/xyz$': "modu" } - , 'modu', 'top/xyz/file' ) - - nestedName( { 'top/xyz': "modu/some/file.js" } - , 'modu/some/file.js', Error ) - - nestedName( { 'top/xyz': "modu/dir" } - , 'modu/dir', 'modu/dir/file' ) - - nestedName( { 'top/xyz': "top/xyz/dir" } - , 'top/xyz/dir', 'top/xyz/dir/file' ) - - nestedName( { 'top/xyz$': "top/xyz/dir" } - , 'top/xyz/dir', 'top/xyz/file' ) -}) +// nestedName( { 'top/xyz$': "top/xyz/dir" } +// , 'top/xyz/dir', 'top/xyz/file' ) +// }) diff --git a/resolvers/webpack/test/externals.js b/resolvers/webpack/test/externals.js index a9a97b87cc..e2e61fbe19 100644 --- a/resolvers/webpack/test/externals.js +++ b/resolvers/webpack/test/externals.js @@ -19,6 +19,12 @@ describe("externals", function () { expect(resolved).to.have.property('path', null) }) + it("works on a function", function () { + var resolved = webpack.resolve('underscore', file) + expect(resolved).to.have.property('found', true) + expect(resolved).to.have.property('path', null) + }) + it("returns null for core modules", function () { var resolved = webpack.resolve('fs', file) expect(resolved).to.have.property('found', true) diff --git a/resolvers/webpack/test/files/webpack.config.js b/resolvers/webpack/test/files/webpack.config.js index f1ab03c6c5..5bc10be97d 100644 --- a/resolvers/webpack/test/files/webpack.config.js +++ b/resolvers/webpack/test/files/webpack.config.js @@ -13,5 +13,11 @@ module.exports = { externals: [ { 'jquery': 'jQuery' }, 'bootstrap', + function (context, request, callback) { + if (request === 'underscore') { + return callback(null, 'underscore'); + }; + callback(); + } ], } diff --git a/resolvers/webpack/test/loaders.js b/resolvers/webpack/test/loaders.js new file mode 100644 index 0000000000..1d588c77cd --- /dev/null +++ b/resolvers/webpack/test/loaders.js @@ -0,0 +1,35 @@ +var chai = require('chai') + , expect = chai.expect + , path = require('path') + +var resolve = require('../index').resolve + + +var file = path.join(__dirname, 'files', 'dummy.js') + +describe("inline loader syntax", function () { + + it("strips bang-loaders", function () { + expect(resolve('css-loader!./src/main-module', file)).to.have.property('path') + .and.equal(path.join(__dirname, 'files', 'src', 'main-module.js')) + }) + + it("strips loader query string", function () { + expect(resolve('some-loader?param=value!./src/main-module', file)).to.have.property('path') + .and.equal(path.join(__dirname, 'files', 'src', 'main-module.js')) + }) + + it("strips resource query string", function () { + expect(resolve('./src/main-module?otherParam=otherValue', file)) + .to.have.property('path') + .and.equal(path.join(__dirname, 'files', 'src', 'main-module.js')) + }) + + it("strips everything", function () { + expect(resolve('some-loader?param=value!./src/main-module?otherParam=otherValue', file)) + .to.have.property('path') + .and.equal(path.join(__dirname, 'files', 'src', 'main-module.js')) + }) + +}) + diff --git a/src/core/getExports.js b/src/core/getExports.js index befcc73d7a..f0cf676144 100644 --- a/src/core/getExports.js +++ b/src/core/getExports.js @@ -97,6 +97,12 @@ export default class ExportMap { return m // can't continue } + const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc'] + const docStyleParsers = {} + docstyle.forEach(style => { + docStyleParsers[style] = availableDocStyleParsers[style] + }) + // attempt to collect module doc ast.comments.some(c => { if (c.type !== 'Block') return false @@ -143,7 +149,7 @@ export default class ExportMap { ast.body.forEach(function (n) { if (n.type === 'ExportDefaultDeclaration') { - const exportMeta = captureDoc(n) + const exportMeta = captureDoc(docStyleParsers, n) if (n.declaration.type === 'Identifier') { addNamespace(exportMeta, n.declaration) } @@ -174,11 +180,12 @@ export default class ExportMap { case 'FunctionDeclaration': case 'ClassDeclaration': case 'TypeAlias': // flowtype with babel-eslint parser - m.namespace.set(n.declaration.id.name, captureDoc(n)) + m.namespace.set(n.declaration.id.name, captureDoc(docStyleParsers, n)) break case 'VariableDeclaration': n.declaration.declarations.forEach((d) => - recursivePatternCapture(d.id, id => m.namespace.set(id.name, captureDoc(d, n)))) + recursivePatternCapture(d.id, id => + m.namespace.set(id.name, captureDoc(docStyleParsers, d, n)))) break } } @@ -348,33 +355,82 @@ export default class ExportMap { } /** - * parse JSDoc from the first node that has leading comments + * parse docs from the first node that has leading comments * @param {...[type]} nodes [description] * @return {{doc: object}} */ -function captureDoc(...nodes) { +function captureDoc(docStyleParsers, ...nodes) { const metadata = {} // 'some' short-circuits on first 'true' nodes.some(n => { if (!n.leadingComments) return false - // capture XSDoc - n.leadingComments.forEach(comment => { - // skip non-block comments - if (comment.value.slice(0, 4) !== '*\n *') return - try { - metadata.doc = doctrine.parse(comment.value, { unwrap: true }) - } catch (err) { - /* don't care, for now? maybe add to `errors?` */ + for (let name in docStyleParsers) { + const doc = docStyleParsers[name](n.leadingComments) + if (doc) { + metadata.doc = doc } - }) + } + return true }) return metadata } +const availableDocStyleParsers = { + jsdoc: captureJsDoc, + tomdoc: captureTomDoc, +} + +/** + * parse JSDoc from leading comments + * @param {...[type]} comments [description] + * @return {{doc: object}} + */ +function captureJsDoc(comments) { + let doc + + // capture XSDoc + comments.forEach(comment => { + // skip non-block comments + if (comment.value.slice(0, 4) !== '*\n *') return + try { + doc = doctrine.parse(comment.value, { unwrap: true }) + } catch (err) { + /* don't care, for now? maybe add to `errors?` */ + } + }) + + return doc +} + +/** + * parse TomDoc section from comments + */ +function captureTomDoc(comments) { + // collect lines up to first paragraph break + const lines = [] + for (let i = 0; i < comments.length; i++) { + const comment = comments[i] + if (comment.value.match(/^\s*$/)) break + lines.push(comment.value.trim()) + } + + // return doctrine-like object + const statusMatch = lines.join(' ').match(/^(Public|Internal|Deprecated):\s*(.+)/) + if (statusMatch) { + return { + description: statusMatch[2], + tags: [{ + title: statusMatch[1].toLowerCase(), + description: statusMatch[2], + }], + } + } +} + /** * Traverse a pattern/identifier node, calling 'callback' * for each leaf identifier. diff --git a/src/core/resolve.js b/src/core/resolve.js index 9479bd553b..93b39d10f1 100644 --- a/src/core/resolve.js +++ b/src/core/resolve.js @@ -2,9 +2,13 @@ import 'es6-symbol/implement' import Map from 'es6-map' import Set from 'es6-set' import assign from 'object-assign' +import pkgDir from 'pkg-dir' +import isAbsoluteFallback from 'is-absolute' import fs from 'fs' -import { dirname, basename, join } from 'path' +import { dirname, basename, join, isAbsolute as isAbsoluteNode } from 'path' + +const isAbsolute = isAbsoluteNode || isAbsoluteFallback export const CASE_SENSITIVE_FS = !fs.existsSync(join(__dirname, 'reSOLVE.js')) @@ -106,7 +110,7 @@ export function relative(modulePath, sourceFile, settings) { const resolvers = resolverReducer(configResolvers, new Map()) for (let [name, config] of resolvers) { - const resolver = requireResolver(name) + const resolver = requireResolver(name, sourceFile) let { path: fullPath, found } = withResolver(resolver, config) @@ -143,12 +147,32 @@ function resolverReducer(resolvers, map) { throw new Error('invalid resolver config') } -function requireResolver(name) { +function requireResolver(name, sourceFile) { + // Try to resolve package with absolute path (/Volumes/....) + if (isAbsolute(name)) { + try { + return require(name) + } catch (err) { /* continue */ } + } + + // Try to resolve package with path, relative to closest package.json + try { + const packageDir = pkgDir.sync(sourceFile) + return require(join(packageDir, name)) + } catch (err) { /* continue */ } + + // Try to resolve package with custom name (@myorg/resolver-name) + try { + return require(name) + } catch (err) { /* continue */ } + + // Try to resolve package with conventional name try { return require(`eslint-import-resolver-${name}`) - } catch (err) { - throw new Error(`unable to load resolver "${name}".`) - } + } catch (err) { /* continue */ } + + // all else failed + throw new Error(`unable to load resolver "${name}".`) } const erroredContexts = new Set() diff --git a/src/rules/prefer-default-export.js b/src/rules/prefer-default-export.js index 9e6ce7a8e9..2bd4783ebb 100644 --- a/src/rules/prefer-default-export.js +++ b/src/rules/prefer-default-export.js @@ -38,6 +38,10 @@ module.exports = function(context) { captureDeclaration(declaration.id) }) } + else { + // captures 'export function foo() {}' syntax + specifierExportCount++ + } namedExportNode = node }, diff --git a/tests/files/foo-bar-resolver.js b/tests/files/foo-bar-resolver.js new file mode 100644 index 0000000000..92421ba26c --- /dev/null +++ b/tests/files/foo-bar-resolver.js @@ -0,0 +1,7 @@ +var path = require('path'); + +exports.resolve = function(source, file) { + return { found: true, path: path.join(__dirname, 'bar.jsx') }; +}; + +exports.interfaceVersion = 2; diff --git a/tests/files/tomdoc-deprecated.js b/tests/files/tomdoc-deprecated.js new file mode 100644 index 0000000000..d7b8bb217f --- /dev/null +++ b/tests/files/tomdoc-deprecated.js @@ -0,0 +1,22 @@ +// Deprecated: This function is terrible. +// +// With another line comment in description. +export function fn() { return null } + +// Deprecated: this is awful, +// use NotAsBadClass. +// +// Some other description text. +export default class TerribleClass { + +} + +// Deprecated: Please stop sending/handling this action type. +export const MY_TERRIBLE_ACTION = "ugh" + +// Public: This one is fine. +// +// Returns a String "great!" +export function fine() { return "great!" } + +export function _undocumented() { return "sneaky!" } diff --git a/tests/files/webpack.config.js b/tests/files/webpack.config.js index 93986f37a3..980c32425e 100644 --- a/tests/files/webpack.config.js +++ b/tests/files/webpack.config.js @@ -1,6 +1,6 @@ module.exports = { resolve: { - extensions: ['.js', '.jsx'], + extensions: ['', '.js', '.jsx'], root: __dirname, }, } diff --git a/tests/src/core/resolve.js b/tests/src/core/resolve.js index cc2f6bd536..95025b34d0 100644 --- a/tests/src/core/resolve.js +++ b/tests/src/core/resolve.js @@ -10,6 +10,14 @@ describe('resolve', function () { expect(resolve.bind(null, null, null)).to.throw(Error) }) + it('loads a custom resolver path', function () { + var file = resolve( '../files/foo' + , utils.testContext({ 'import/resolver': './foo-bar-resolver'}) + ) + + expect(file).to.equal(utils.testFilePath('./bar.jsx')) + }) + it('respects import/resolve extensions', function () { var file = resolve( './jsx/MyCoolComponent' , utils.testContext({ 'import/resolve': { 'extensions': ['.jsx'] }}) diff --git a/tests/src/rules/no-deprecated.js b/tests/src/rules/no-deprecated.js index 92660348b2..ef6912dd1e 100644 --- a/tests/src/rules/no-deprecated.js +++ b/tests/src/rules/no-deprecated.js @@ -13,6 +13,20 @@ ruleTester.run('no-deprecated', rule, { test({ code: "import { fine } from './deprecated'" }), test({ code: "import { _undocumented } from './deprecated'" }), + test({ + code: "import { fn } from './deprecated'", + settings: { 'import/docstyle': ['tomdoc'] } + }), + + test({ + code: "import { fine } from './tomdoc-deprecated'", + settings: { 'import/docstyle': ['tomdoc'] } + }), + test({ + code: "import { _undocumented } from './tomdoc-deprecated'", + settings: { 'import/docstyle': ['tomdoc'] } + }), + // naked namespace is fine test({ code: "import * as depd from './deprecated'" }), test({ code: "import * as depd from './deprecated'; console.log(depd.fine())" }), @@ -44,6 +58,30 @@ ruleTester.run('no-deprecated', rule, { errors: ['Deprecated: please stop sending/handling this action type.'], }), + test({ + code: "import { fn } from './deprecated'", + settings: { 'import/docstyle': ['jsdoc', 'tomdoc'] }, + errors: ["Deprecated: please use 'x' instead."], + }), + + test({ + code: "import { fn } from './tomdoc-deprecated'", + settings: { 'import/docstyle': ['tomdoc'] }, + errors: ["Deprecated: This function is terrible."], + }), + + test({ + code: "import TerribleClass from './tomdoc-deprecated'", + settings: { 'import/docstyle': ['tomdoc'] }, + errors: ['Deprecated: this is awful, use NotAsBadClass.'], + }), + + test({ + code: "import { MY_TERRIBLE_ACTION } from './tomdoc-deprecated'", + settings: { 'import/docstyle': ['tomdoc'] }, + errors: ['Deprecated: Please stop sending/handling this action type.'], + }), + // ignore redeclares test({ code: "import { MY_TERRIBLE_ACTION } from './deprecated'; function shadow(MY_TERRIBLE_ACTION) { console.log(MY_TERRIBLE_ACTION); }", diff --git a/tests/src/rules/prefer-default-export.js b/tests/src/rules/prefer-default-export.js index c38eff0449..33eee694d3 100644 --- a/tests/src/rules/prefer-default-export.js +++ b/tests/src/rules/prefer-default-export.js @@ -12,6 +12,15 @@ ruleTester.run('prefer-default-export', rule, { export const foo = 'foo'; export const bar = 'bar';`, }), + test({ + code: ` + export default function bar() {};`, + }), + test({ + code: ` + export const foo = 'foo'; + export function bar() {};`, + }), test({ code: ` export const foo = 'foo'; @@ -56,6 +65,14 @@ ruleTester.run('prefer-default-export', rule, { // ...SYNTAX_CASES, ], invalid: [ + test({ + code: ` + export function bar() {};`, + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Prefer default export.', + }], + }), test({ code: ` export const foo = 'foo';`,