diff --git a/.eslintrc.js b/.eslintrc.js index e780300..fe46b33 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,25 +1,22 @@ module.exports = { - "env": { - "commonjs": true, - "es6": true - }, - "extends": "eslint:recommended", - "rules": { - "indent": [ - "error", - 2 - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "single" - ], - "semi": [ - "error", - "always" - ] - } + env: { + commonjs: true, + es6: true, + node: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:import/errors', + 'plugin:import/warnings', + ], + plugins: [ + 'import', + ], + rules: { + indent: ['error', 2], + 'linebreak-style': ['error', 'unix'], + quotes: ['error', 'single'], + semi: ['error', 'always'], + 'import/no-unresolved': [2, { commonjs: true, amd: true }] + } }; diff --git a/README.md b/README.md index 716d256..b58daa2 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,19 @@ settings: kibana: { kibanaPath: '/path/to/kibana' } ``` -See [the resolvers docs](https://github.com/benmosher/eslint-plugin-import#resolvers) or the [resolver spec](https://github.com/benmosher/eslint-plugin-import/blob/master/resolvers/README.md#resolvesource-file-config---found-boolean-path-string-) for more details. \ No newline at end of file +See [the resolvers docs](https://github.com/benmosher/eslint-plugin-import#resolvers) or the [resolver spec](https://github.com/benmosher/eslint-plugin-import/blob/master/resolvers/README.md#resolvesource-file-config---found-boolean-path-string-) for more details. + +## Configuration + +Property | Default | Descritpion +-------- | ------- | ----------- +kibanaPath | `../kibana` | Relative path to the kibana root +rootPackageName | | The `name` property in the root `package.json` file, required when your plugin has multiple plugins with their own`package.json` files +pluginDirs | `[]` | Array of additional directories to check for Kibana plugins +pluginPaths | `[]` | Array of additional paths to look in when resolving plugin dependencies + +## Debugging + +For debugging output from this resolver, run your linter with `DEBUG=eslint-plugin-import:resolver:kibana`. + +This resolver makes heavy use of *eslint-import-resolver-webpack*, and you can get debugging output from both resolver by using `DEBUG=eslint-plugin-import:resolver:*`. \ No newline at end of file diff --git a/index.js b/index.js index 37e2a55..447fc45 100644 --- a/index.js +++ b/index.js @@ -1,213 +1,18 @@ -const path = require('path'); -const findRoot = require('find-root'); -const glob = require('glob'); -const debug = require('debug')('eslint-import-resolver-kibana'); +const webpackResolver = require('eslint-import-resolver-webpack'); +const getProjectRoot = require('./lib/get_project_root'); +const getWebpackConfig = require('./lib/get_webpack_config'); -const defaults = { - kibanaPath: '../kibana', -}; - -/* - * Resolves the path to Kibana, either from default setting or config - */ -function getKibanaPath(config, file, rootPath) { - const inConfig = config != null && config.kibanaPath; - - const kibanaPath = (inConfig) - ? path.resolve(config.kibanaPath) - : path.resolve(rootPath, defaults.kibanaPath); - - debug(`resolved kibana path: ${kibanaPath}`); - return kibanaPath; -} - -/* - * Creates a glob pattern string that looks for: - * - file that matches the source (source.js) - * - directory with an index.js that matches the source (source/index.js) - * - directory with a matching module name that matches the source (source/source.js) - * NOTE: last condition mentioned above can be removed when the custom resolver is removed from Kibana webpack config - * @param {String|Array} source: the module identifier (./imported-file). - */ -function getGlobPattern(source) { - if (Array.isArray(source)) { - const rootPath = path.join(...source); - const filename = source[source.length - 1]; - return `./${rootPath}{*(.js),/${filename}.js,/index.js}`; - } else { - return `./${source}{*(.js),/${source}.js,/index.js}`; - } -} - -/* - * Returns an array of relative file path strings that match the source - * @param {String} source: the module identifier - * @param {String} checkPath: path to search in for file globbing - */ -function getFileMatches(source, checkPath) { - const globPattern = getGlobPattern(source); - const globOptions = { - cwd: path.resolve(checkPath), - }; - - const matches = glob.sync(globPattern, globOptions); - debug(`checking in ${checkPath}, matched ${matches.length}`); - - return matches; -} - -/* - * Return an object with a found property of `true` or `false` - * If found, returns a `path` property of the matched file - * @param {Array} matches: relative paths to check - * @param {String} checkPath: prefix for the path - */ -function getMatch(matches, checkPath) { - if (Array.isArray(matches) && matches.length >= 1) { - const matchPath = path.resolve(checkPath, matches[matches.length - 1]); - debug(`matched path: ${matchPath}`); - return { - found: true, - path: matchPath, - }; - } - - return { found: false }; -} - -/* - * Resolves imports in local plugin that begin with `plugin/` - * NOTE: this does not resolve across different plugins - * NOTE: when webpack aliases are removed from Kibana, this will no longer be needed. - */ -function resolvePluginsAliasImport(pluginsImport, kibanaPath, rootPath) { - const { name: packageName } = require(path.resolve(rootPath, 'package.json')); - const [ pluginName, ...importPaths ] = pluginsImport[1].split('/'); - debug(`resolvePluginsAliasImport: package ${packageName}, plugin ${pluginName}, import ${importPaths.join('/')}`); - - if (packageName !== 'kibana' && packageName === pluginName) { - // resolve local plugin path - const checkPath = path.join(rootPath, 'public'); - const matches = getFileMatches(importPaths, checkPath); - return getMatch(matches, checkPath); - } else { - // resolve kibana core plugin path - const checkPath = path.join(kibanaPath, 'src', 'core_plugins', pluginName, 'public'); - const matches = getFileMatches(importPaths, checkPath); - return getMatch(matches, checkPath); - } -} +// cache expensive resolution results +let projectRoot; +let webpackConfig; -/* - * Resolves imports where source is a directory with relative path and has a source file with the same name - * @param {Array} fileImport: source of relative - * @param {String} file: absolute path to the file making the import - * @param {String} rootPath: root path of the project code - */ -function resolveLocalRelativeImport(fileImport, file) { - const sourceBase = path.basename(fileImport); - const localPath = path.dirname(path.resolve(path.dirname(file), fileImport)); - debug(`resolving relative path: ${localPath}`); - const matches = getFileMatches(sourceBase, localPath); - return getMatch(matches, localPath); -} - -/* - * Attempts to resolve imports as webpackShims, either in Kibana or in the local plugin - * @param {String} source: the module identifier - * @param {String} kibanaPath: path to Kibana, default or configured - * @param {String} rootPath: root path of the project code - */ -function resolveWebpackShim(source, kibanaPath, rootPath) { - const pluginShimPath = path.join(rootPath, 'webpackShims'); - const pluginMatches = getFileMatches(source, pluginShimPath); - const pluginFileMatches = getMatch(pluginMatches, pluginShimPath); - debug(`resolveWebpackShim: checking for ${source}`); - if (pluginFileMatches.found) { - debug(`resolved webpackShim import in plugin: ${source}`); - return pluginFileMatches; - } - - const kibanaShimPath = path.join(kibanaPath, 'webpackShims'); - const kibanaMatches = getFileMatches(source, kibanaShimPath); - const kibanaFileMatches = getMatch(kibanaMatches, kibanaShimPath); - if (kibanaFileMatches.found) { - debug(`resolved webpackShim import in Kibana: ${source}`); - } - return kibanaFileMatches; -} - -/* - * Resolves global import aliases that map a prefix to a Kibana source directory - * Used for UI: https://github.com/elastic/kibana/blob/5c04ff65fbb3b16f8958f8241488463136415670/src/ui/ui_bundler_env.js#L29 - * Used for tests: https://github.com/elastic/kibana/blob/5c04ff65fbb3b16f8958f8241488463136415670/src/core_plugins/tests_bundle/index.js#L70-L75 - */ -function resolveKibanaModuleImport(source, kibanaPath) { - debug(`resolveKibanaModuleImport: checking ${kibanaPath} for ${source}`); - const checkPaths = [ - path.join(kibanaPath), // ui_framework/components - path.join(kibanaPath, 'src', 'core_plugins', 'dev_mode', 'public'), // ng_mock - path.join(kibanaPath, 'src', 'fixtures'), // fixtures/something - path.join(kibanaPath, 'src', 'test_harness', 'public'), // test_harness/something - path.join(kibanaPath, 'src', 'test_utils', 'public'), // test_utils/something - path.join(kibanaPath, 'src', 'ui', 'public'), // ui/something - ]; - - // clean the source - // strip off leading prefix, if its there (ui, fixtures, test_utils, test_harness) - const baseSource = source.replace(/^(ui|fixtures|test_utils|test_harness)\//, ''); - let resolved = { found: false }; - checkPaths.forEach(function (checkPath) { - if (!resolved.found) { - debug(`resolveKibanaModuleImport: check for ${baseSource} in ${checkPath}`); - const matches = getFileMatches(baseSource, checkPath); - const match = getMatch(matches, checkPath); - if (match.found) { - debug(`resolving ui import ${source} as ${match.path}`); - resolved = match; - } - } - }); - return resolved; -} - -function stripPath(strip, result) { - if (!result.found || !strip) return result; - return { - found: result.found, - path: null, - }; -} - -/* - * See - * https://github.com/benmosher/eslint-plugin-import/blob/master/resolvers/README.md#resolvesource-file-config---found-boolean-path-string- - * @param {String} source: the module identifier (./imported-file). - * @param {String} file: the absolute path to the file making the import (/some/path/to/module.js) - * @param {Object} config: an object provided via the import/resolver setting. - */ exports.resolve = function resolveKibanaPath(source, file, config) { - const rootPath = findRoot(file); - const kibanaPath = getKibanaPath(config, file, rootPath); - - const loaderPrefix = source.match(/\!*(raw|file.+)\!+(.*)/); - const loaderPrefixed = (loaderPrefix !== null); - const realSource = (loaderPrefixed) ? loaderPrefix[2] : source; - const pathFix = (result) => stripPath(loaderPrefixed, result); - - // check relative paths - const relativeImport = Boolean(realSource.match(new RegExp('^\\.\\.?/(.*)'))); - if (relativeImport) return pathFix(resolveLocalRelativeImport(realSource, file)); + projectRoot = projectRoot || getProjectRoot(file, config); + webpackConfig = webpackConfig || getWebpackConfig(source, projectRoot, config); - // check local plugins path (resolves kibana webpack alias) - const pluginsImport = realSource.match(new RegExp('^plugins/(.*)')); - if (pluginsImport !== null) return pathFix(resolvePluginsAliasImport(pluginsImport, kibanaPath, rootPath)); - - // check for matches in kibana (using kibana webpack aliases) - const aliasModuleImport = pathFix(resolveKibanaModuleImport(realSource, kibanaPath)); - if (aliasModuleImport.found) return aliasModuleImport; - - return resolveWebpackShim(realSource, kibanaPath, rootPath); + return webpackResolver.resolve(source, file, { + config: webpackConfig + }); }; // use version 2 of the resolver interface, https://github.com/benmosher/eslint-plugin-import/blob/master/resolvers/README.md#interfaceversion--number diff --git a/lib/debug.js b/lib/debug.js new file mode 100644 index 0000000..5ec22cf --- /dev/null +++ b/lib/debug.js @@ -0,0 +1,3 @@ +const debug = require('debug')('eslint-plugin-import:resolver:kibana'); + +module.exports = debug; diff --git a/lib/defaults.js b/lib/defaults.js new file mode 100644 index 0000000..4b35cb8 --- /dev/null +++ b/lib/defaults.js @@ -0,0 +1,5 @@ +module.exports = { + kibanaPath: '../kibana', + pluginDirs: [], + pluginPaths: [], +}; \ No newline at end of file diff --git a/lib/get_kibana_path.js b/lib/get_kibana_path.js new file mode 100644 index 0000000..967e0dd --- /dev/null +++ b/lib/get_kibana_path.js @@ -0,0 +1,21 @@ +const { resolve } = require('path'); +const debug = require('./debug'); +const defaults = require('./defaults'); + +let kibanaPath; + +/* + * Resolves the path to Kibana, either from default setting or config + */ +module.exports = function getKibanaPath(config, projectRoot) { + if (kibanaPath) return kibanaPath; + + const inConfig = config != null && config.kibanaPath; + + kibanaPath = (inConfig) + ? resolve(config.kibanaPath) + : resolve(projectRoot, defaults.kibanaPath); + + debug(`Resolved Kibana path: ${kibanaPath}`); + return kibanaPath; +}; \ No newline at end of file diff --git a/lib/get_plugins.js b/lib/get_plugins.js new file mode 100644 index 0000000..583d6f4 --- /dev/null +++ b/lib/get_plugins.js @@ -0,0 +1,28 @@ +const { dirname, resolve } = require('path'); +const glob = require('glob-all'); + +const defaults = require('./defaults'); + +module.exports = function getPlugins(config, kibanaPath, projectRoot) { + const pluginDirs = [ + ...(config.pluginDirs || defaults.pluginDirs), + resolve(kibanaPath, 'plugins'), + resolve(kibanaPath, 'src', 'core_plugins'), + ]; + + const globPatterns = [ + ...pluginDirs.map(dir => `${dir}/*/package.json`), + ...(config.pluginPaths || defaults.pluginPaths).map(path => `${path}/package.json`), + ]; + const globOptions = { cwd: projectRoot }; + + return glob.sync(globPatterns, globOptions).map(pkgJsonPath => { + const path = dirname(pkgJsonPath); + const pkg = require(pkgJsonPath); + return { + name: pkg.name, + directory: path, + publicDirectory: resolve(path, 'public'), + }; + }); +}; diff --git a/lib/get_project_root.js b/lib/get_project_root.js new file mode 100644 index 0000000..2a37c05 --- /dev/null +++ b/lib/get_project_root.js @@ -0,0 +1,36 @@ +const { dirname, resolve, parse } = require('path'); +const { accessSync, readFileSync } = require('fs'); +const debug = require('./debug'); + +function getRootPackageDir(dirRoot, dir, rootPackageName) { + if (dirRoot === dir) return null; + const pkgFile = resolve(dir, 'package.json'); + + try { + accessSync(pkgFile); + + // if rootPackageName is not provided, stop when package.json is found + if (!rootPackageName) return dir; + + // if rootPackageName is provided, check for match + const { name } = JSON.parse(readFileSync(pkgFile)); + if (name === rootPackageName) return dir; + + // recurse until a matching package.json is found + return getRootPackageDir(dirRoot, dirname(dir), rootPackageName); + } catch (e) { + if (e.code === 'ENOENT') return getRootPackageDir(dirRoot, dirname(dir), rootPackageName); + throw e; + } +} + +module.exports = function getProjectRoot(file, config) { + const { root, dir } = parse(resolve(file)); + const { rootPackageName } = config; + + projectRoot = getRootPackageDir(root, dir, rootPackageName); + if (projectRoot === null) throw new Error('Failed to find plugin root'); + + debug(`Resolved project root: ${projectRoot}`); + return projectRoot; +}; diff --git a/lib/get_webpack_config.js b/lib/get_webpack_config.js new file mode 100644 index 0000000..d15f07a --- /dev/null +++ b/lib/get_webpack_config.js @@ -0,0 +1,53 @@ +const { resolve } = require('path'); +const webpack = require('webpack'); +const DirectoryNameAsMain = require('@elastic/webpack-directory-name-as-main'); +const debug = require('./debug'); + +const getKibanaPath = require('./get_kibana_path'); +const getPlugins = require('./get_plugins'); + +module.exports = function getWebpackConfig(source, projectRoot, config) { + const kibanaPath = getKibanaPath(config, projectRoot); + const fromKibana = (...path) => resolve(kibanaPath, ...path); + + const plugins = [ new webpack.ResolverPlugin([ new DirectoryNameAsMain() ]) ]; + const aliases = { + // Kibana defaults https://github.com/elastic/kibana/blob/b7f519704ae0d25f085e3278198e2abec1d9ef6e/src/ui/ui_bundler_env.js#L30-L34 + ui: fromKibana('src/ui/public'), + test_harness: fromKibana('src/test_harness/public'), + querystring: 'querystring-browser', + + // Dev defaults for test bundle https://github.com/elastic/kibana/blob/b7f519704ae0d25f085e3278198e2abec1d9ef6e/src/core_plugins/tests_bundle/index.js#L70-L75 + ng_mock$: fromKibana('src/core_plugins/dev_mode/public/ng_mock'), + 'angular-mocks$': fromKibana('src/core_plugins/tests_bundle/webpackShims/angular-mocks.js'), + fixtures: fromKibana('src/fixtures'), + test_utils: fromKibana('src/test_utils/public'), + }; + + getPlugins(config, kibanaPath, projectRoot).forEach(plugin => { + aliases[`plugins/${plugin.name}`] = plugin.publicDirectory; + }); + + debug('Webpack resolved plugins', plugins); + debug('Webpack resolved aliases', aliases); + + webpackConfig = { + context: kibanaPath, + plugins, + resolve: { + extensions: ['.js', '.json', '.jsx', '.less', ''], + postfixes: [''], + modulesDirectories: ['webpackShims', 'node_modules'], + fallback: [ + fromKibana('webpackShims'), + fromKibana('node_modules') + ], + loaderPostfixes: ['-loader', ''], + root: fromKibana('.'), + alias: aliases, + unsafeCache: true, + }, + }; + + return webpackConfig; +}; diff --git a/package.json b/package.json index ec8d017..40d54ba 100644 --- a/package.json +++ b/package.json @@ -5,16 +5,22 @@ "main": "index.js", "author": "Joe Fleming ", "license": "MIT", + "scripts": { + "lint": "eslint index.js" + }, "repository": { "type": "git", "url": "git+https://github.com/elastic/eslint-import-resolver-kibana.git" }, "dependencies": { + "@elastic/webpack-directory-name-as-main": "^2.0.3", "debug": "^2.6.6", - "find-root": "^1.0.0", - "glob": "^7.1.1" + "eslint-import-resolver-webpack": "^0.8.1", + "glob-all": "^3.1.0", + "webpack": "github:elastic/webpack#fix/query-params-for-aliased-loaders" }, "devDependencies": { - "eslint": "^3.19.0" + "eslint": "^3.19.0", + "eslint-plugin-import": "^2.3.0" } }