Skip to content

Commit

Permalink
Update: Resolve npm installed formatters (#5900) (#9464)
Browse files Browse the repository at this point in the history
  • Loading branch information
testower authored and ilyavolodin committed Nov 1, 2017
1 parent accc490 commit 4d7d7ab
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 138 deletions.
11 changes: 11 additions & 0 deletions docs/user-guide/command-line-interface.md
Expand Up @@ -333,6 +333,17 @@ Example:

eslint -f ./customformat.js file.js

An npm-installed formatter is resolved with or without `eslint-formatter-` prefix.

Example:

npm install eslint-formatter-pretty

eslint -f pretty file.js

// equivalent:
eslint -f eslint-formatter-pretty file.js

When specified, the given format is output to the console. If you'd like to save that output into a file, you can do so on the command line like so:

eslint -f compact file.js > results.txt
Expand Down
19 changes: 15 additions & 4 deletions lib/cli-engine.js
Expand Up @@ -26,10 +26,14 @@ const fs = require("fs"),
validator = require("./config/config-validator"),
stringify = require("json-stable-stringify"),
hash = require("./util/hash"),
ModuleResolver = require("./util/module-resolver"),
naming = require("./util/naming"),
pkg = require("../package.json");

const debug = require("debug")("eslint:cli-engine");

const resolver = new ModuleResolver();

//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -670,15 +674,22 @@ class CLIEngine {
// replace \ with / for Windows compatibility
format = format.replace(/\\/g, "/");

const cwd = this.options ? this.options.cwd : process.cwd();
const namespace = naming.getNamespaceFromTerm(format);

let formatterPath;

// if there's a slash, then it's a file
if (format.indexOf("/") > -1) {
const cwd = this.options ? this.options.cwd : process.cwd();

if (!namespace && format.indexOf("/") > -1) {
formatterPath = path.resolve(cwd, format);
} else {
formatterPath = `./formatters/${format}`;
try {
const npmFormat = naming.normalizePackageName(format, "eslint-formatter");

formatterPath = resolver.resolve(npmFormat, `${cwd}/node_modules`);
} catch (e) {
formatterPath = `./formatters/${format}`;
}
}

try {
Expand Down
51 changes: 3 additions & 48 deletions lib/config/config-file.js
Expand Up @@ -13,8 +13,8 @@ const fs = require("fs"),
path = require("path"),
ConfigOps = require("./config-ops"),
validator = require("./config-validator"),
pathUtil = require("../util/path-util"),
ModuleResolver = require("../util/module-resolver"),
naming = require("../util/naming"),
pathIsInside = require("path-is-inside"),
stripComments = require("strip-json-comments"),
stringify = require("json-stable-stringify"),
Expand Down Expand Up @@ -436,50 +436,6 @@ function applyExtends(config, configContext, filePath, relativeTo) {
return config;
}

/**
* Brings package name to correct format based on prefix
* @param {string} name The name of the package.
* @param {string} prefix Can be either "eslint-plugin" or "eslint-config
* @returns {string} Normalized name of the package
* @private
*/
function normalizePackageName(name, prefix) {

/*
* On Windows, name can come in with Windows slashes instead of Unix slashes.
* Normalize to Unix first to avoid errors later on.
* https://github.com/eslint/eslint/issues/5644
*/
if (name.indexOf("\\") > -1) {
name = pathUtil.convertPathToPosix(name);
}

if (name.charAt(0) === "@") {

/*
* it's a scoped package
* package name is "eslint-config", or just a username
*/
const scopedPackageShortcutRegex = new RegExp(`^(@[^/]+)(?:/(?:${prefix})?)?$`),
scopedPackageNameRegex = new RegExp(`^${prefix}(-|$)`);

if (scopedPackageShortcutRegex.test(name)) {
name = name.replace(scopedPackageShortcutRegex, `$1/${prefix}`);
} else if (!scopedPackageNameRegex.test(name.split("/")[1])) {

/*
* for scoped packages, insert the eslint-config after the first / unless
* the path is already @scope/eslint or @scope/eslint-config-xxx
*/
name = name.replace(/^@([^/]+)\/(.*)$/, `@$1/${prefix}-$2`);
}
} else if (name.indexOf(`${prefix}-`) !== 0) {
name = `${prefix}-${name}`;
}

return name;
}

/**
* Resolves a configuration file path into the fully-formed path, whether filename
* or package name.
Expand All @@ -505,12 +461,12 @@ function resolve(filePath, relativeTo) {
const pluginName = filePath.slice(7, filePath.lastIndexOf("/"));
const configName = filePath.slice(filePath.lastIndexOf("/") + 1);

normalizedPackageName = normalizePackageName(pluginName, "eslint-plugin");
normalizedPackageName = naming.normalizePackageName(pluginName, "eslint-plugin");
debug(`Attempting to resolve ${normalizedPackageName}`);
filePath = resolver.resolve(normalizedPackageName, getLookupPath(relativeTo));
return { filePath, configName, configFullName };
}
normalizedPackageName = normalizePackageName(filePath, "eslint-config");
normalizedPackageName = naming.normalizePackageName(filePath, "eslint-config");
debug(`Attempting to resolve ${normalizedPackageName}`);
filePath = resolver.resolve(normalizedPackageName, getLookupPath(relativeTo));
return { filePath, configFullName: filePath };
Expand Down Expand Up @@ -614,7 +570,6 @@ module.exports = {
resolve,
write,
applyExtends,
normalizePackageName,
CONFIG_FILES,

/**
Expand Down
43 changes: 8 additions & 35 deletions lib/config/plugins.js
Expand Up @@ -9,13 +9,13 @@
//------------------------------------------------------------------------------

const debug = require("debug")("eslint:plugins");
const naming = require("../util/naming");

//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------

const PLUGIN_NAME_PREFIX = "eslint-plugin-",
NAMESPACE_REGEX = /^@.*\//i;
const PLUGIN_NAME_PREFIX = "eslint-plugin-";

//------------------------------------------------------------------------------
// Public Interface
Expand All @@ -37,43 +37,16 @@ class Plugins {
this._rules = rulesContext;
}

/**
* Removes the prefix `eslint-plugin-` from a plugin name.
* @param {string} pluginName The name of the plugin which may have the prefix.
* @returns {string} The name of the plugin without prefix.
*/
static removePrefix(pluginName) {
return pluginName.startsWith(PLUGIN_NAME_PREFIX) ? pluginName.slice(PLUGIN_NAME_PREFIX.length) : pluginName;
}

/**
* Gets the scope (namespace) of a plugin.
* @param {string} pluginName The name of the plugin which may have the prefix.
* @returns {string} The name of the plugins namepace if it has one.
*/
static getNamespace(pluginName) {
return pluginName.match(NAMESPACE_REGEX) ? pluginName.match(NAMESPACE_REGEX)[0] : "";
}

/**
* Removes the namespace from a plugin name.
* @param {string} pluginName The name of the plugin which may have the prefix.
* @returns {string} The name of the plugin without the namespace.
*/
static removeNamespace(pluginName) {
return pluginName.replace(NAMESPACE_REGEX, "");
}

/**
* Defines a plugin with a given name rather than loading from disk.
* @param {string} pluginName The name of the plugin to load.
* @param {Object} plugin The plugin object.
* @returns {void}
*/
define(pluginName, plugin) {
const pluginNamespace = Plugins.getNamespace(pluginName),
pluginNameWithoutNamespace = Plugins.removeNamespace(pluginName),
pluginNameWithoutPrefix = Plugins.removePrefix(pluginNameWithoutNamespace),
const pluginNamespace = naming.getNamespaceFromTerm(pluginName),
pluginNameWithoutNamespace = naming.removeNamespaceFromTerm(pluginName),
pluginNameWithoutPrefix = naming.removePrefixFromTerm(PLUGIN_NAME_PREFIX, pluginNameWithoutNamespace),
shortName = pluginNamespace + pluginNameWithoutPrefix;

// load up environments and rules
Expand Down Expand Up @@ -106,9 +79,9 @@ class Plugins {
* @throws {Error} If the plugin cannot be loaded.
*/
load(pluginName) {
const pluginNamespace = Plugins.getNamespace(pluginName),
pluginNameWithoutNamespace = Plugins.removeNamespace(pluginName),
pluginNameWithoutPrefix = Plugins.removePrefix(pluginNameWithoutNamespace),
const pluginNamespace = naming.getNamespaceFromTerm(pluginName),
pluginNameWithoutNamespace = naming.removeNamespaceFromTerm(pluginName),
pluginNameWithoutPrefix = naming.removePrefixFromTerm(PLUGIN_NAME_PREFIX, pluginNameWithoutNamespace),
shortName = pluginNamespace + pluginNameWithoutPrefix,
longName = pluginNamespace + PLUGIN_NAME_PREFIX + pluginNameWithoutPrefix;
let plugin = null;
Expand Down
112 changes: 112 additions & 0 deletions lib/util/naming.js
@@ -0,0 +1,112 @@
/**
* @fileoverview Common helpers for naming of plugins, formatters and configs
*/
"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const pathUtil = require("../util/path-util");

//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------

const NAMESPACE_REGEX = /^@.*\//i;

/**
* Brings package name to correct format based on prefix
* @param {string} name The name of the package.
* @param {string} prefix Can be either "eslint-plugin", "eslint-config" or "eslint-formatter"
* @returns {string} Normalized name of the package
* @private
*/
function normalizePackageName(name, prefix) {

/**
* On Windows, name can come in with Windows slashes instead of Unix slashes.
* Normalize to Unix first to avoid errors later on.
* https://github.com/eslint/eslint/issues/5644
*/
if (name.indexOf("\\") > -1) {
name = pathUtil.convertPathToPosix(name);
}

if (name.charAt(0) === "@") {

/**
* it's a scoped package
* package name is the prefix, or just a username
*/
const scopedPackageShortcutRegex = new RegExp(`^(@[^/]+)(?:/(?:${prefix})?)?$`),
scopedPackageNameRegex = new RegExp(`^${prefix}(-|$)`);

if (scopedPackageShortcutRegex.test(name)) {
name = name.replace(scopedPackageShortcutRegex, `$1/${prefix}`);
} else if (!scopedPackageNameRegex.test(name.split("/")[1])) {

/**
* for scoped packages, insert the prefix after the first / unless
* the path is already @scope/eslint or @scope/eslint-xxx-yyy
*/
name = name.replace(/^@([^/]+)\/(.*)$/, `@$1/${prefix}-$2`);
}
} else if (name.indexOf(`${prefix}-`) !== 0) {
name = `${prefix}-${name}`;
}

return name;
}

/**
* Removes the prefix from a term.
* @param {string} prefix The prefix to remove.
* @param {string} term The term which may have the prefix.
* @returns {string} The term without prefix.
*/
function removePrefixFromTerm(prefix, term) {
return term.startsWith(prefix) ? term.slice(prefix.length) : term;
}

/**
* Adds a prefix to a term.
* @param {string} prefix The prefix to add.
* @param {string} term The term which may not have the prefix.
* @returns {string} The term with prefix.
*/
function addPrefixToTerm(prefix, term) {
return term.startsWith(prefix) ? term : `${prefix}${term}`;
}

/**
* Gets the scope (namespace) of a term.
* @param {string} term The term which may have the namespace.
* @returns {string} The namepace of the term if it has one.
*/
function getNamespaceFromTerm(term) {
const match = term.match(NAMESPACE_REGEX);

return match ? match[0] : "";
}

/**
* Removes the namespace from a term.
* @param {string} term The term which may have the namespace.
* @returns {string} The name of the plugin without the namespace.
*/
function removeNamespaceFromTerm(term) {
return term.replace(NAMESPACE_REGEX, "");
}

//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------

module.exports = {
normalizePackageName,
removePrefixFromTerm,
addPrefixToTerm,
getNamespaceFromTerm,
removeNamespaceFromTerm
};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions tests/lib/cli-engine.js
Expand Up @@ -2689,6 +2689,42 @@ describe("CLIEngine", () => {
assert.isFunction(formatter);
});

it("should return a function when a formatter prefixed with eslint-formatter is requested", () => {
const engine = new CLIEngine({
cwd: getFixturePath("cli-engine")
}),
formatter = engine.getFormatter("bar");

assert.isFunction(formatter);
});

it("should return a function when a formatter is requested, also when the eslint-formatter prefix is included in the format argument", () => {
const engine = new CLIEngine({
cwd: getFixturePath("cli-engine")
}),
formatter = engine.getFormatter("eslint-formatter-bar");

assert.isFunction(formatter);
});

it("should return a function when a formatter is requested within a scoped npm package", () => {
const engine = new CLIEngine({
cwd: getFixturePath("cli-engine")
}),
formatter = engine.getFormatter("@somenamespace/foo");

assert.isFunction(formatter);
});

it("should return a function when a formatter is requested within a scoped npm package, also when the eslint-formatter prefix is included in the format argument", () => {
const engine = new CLIEngine({
cwd: getFixturePath("cli-engine")
}),
formatter = engine.getFormatter("@somenamespace/eslint-formatter-foo");

assert.isFunction(formatter);
});

it("should return null when a customer formatter doesn't exist", () => {
const engine = new CLIEngine(),
formatterPath = getFixturePath("formatters", "doesntexist.js");
Expand Down

0 comments on commit 4d7d7ab

Please sign in to comment.