Comparing version 1.5.0 to 1.6.0
@@ -0,1 +1,6 @@ | ||
# v1.6.0 | ||
- Adds `--report lines --prop <PROP[=value]>` option | ||
- Adds config file support | ||
# v1.5.0 | ||
@@ -2,0 +7,0 @@ |
{ | ||
"name": "jsx-info", | ||
"version": "1.5.0", | ||
"version": "1.6.0", | ||
"description": "displays a report of JSX component and prop usage", | ||
@@ -32,2 +32,3 @@ "bin": "src/jsx-info.js", | ||
"commander": "^2.20.0", | ||
"cosmiconfig": "^5.2.1", | ||
"globby": "^9.2.0", | ||
@@ -34,0 +35,0 @@ "ora": "^3.4.0" |
@@ -52,2 +52,29 @@ # jsx-info | ||
Find `<Button kind="primary">` | ||
$ npx jsx-info --report lines --prop kind=primary Button | ||
Find all uses of the prop `id` | ||
$ npx jsx-info --report lines --prop id | ||
## Configuration | ||
In order to avoid repeating command line arguments as often, `jsx-info` supports | ||
reading command line argument defaults from a configuration file. You can either | ||
put defaults in a `.jsx-info.json` file or under a key named `"jsx-info"` in | ||
your `package.json` file. | ||
Either way, your configuration should be JSON that looks like this, where every | ||
key is optional: | ||
```json | ||
{ | ||
"babelPlugins": ["decorators-legacy", "pipelineOperator"], | ||
"directory": "src", | ||
"ignore": ["**/__test__", "legacy/**"], | ||
"files": ["**/*.{js,jsx,tsx}"] | ||
} | ||
``` | ||
## Note | ||
@@ -54,0 +81,0 @@ |
@@ -0,4 +1,7 @@ | ||
const path = require("path"); | ||
const program = require("commander"); | ||
const cosmiconfig = require("cosmiconfig"); | ||
const pkg = require("../package.json"); | ||
const { print, printError } = require("./printer"); | ||
@@ -10,3 +13,3 @@ function listOption(x, acc = []) { | ||
program.name(pkg.name); | ||
program.name("jsx-info"); | ||
program.version(pkg.version, "-v, --version"); | ||
@@ -23,2 +26,3 @@ program | ||
.option("--no-color", "force disable color") | ||
.option("--no-config", "disable config file") | ||
.option("--no-gitignore", "disable reading .gitignore files") | ||
@@ -41,4 +45,4 @@ .option( | ||
"--files <PATTERN>", | ||
"glob pattern used to find input files", | ||
"**/*.{js,jsx,tsx}" | ||
"glob pattern used to find input files (repeatable)", | ||
listOption | ||
) | ||
@@ -51,28 +55,51 @@ .option( | ||
.option( | ||
"--report <usage|props>", | ||
"--report <usage|props|lines>", | ||
"specify reports to show (repeatable)", | ||
listOption | ||
) | ||
.option( | ||
"--prop <PROP>", | ||
"which prop to search for when running `--report lines`" | ||
); | ||
program.on("--help", () => { | ||
// eslint-disable-next-line no-console | ||
console.log(` | ||
print(` | ||
Examples: | ||
# Display info for every component | ||
$ ${pkg.name} | ||
$ npx jsx-info | ||
# Display info only for <div> and <Tab.Container> | ||
$ ${pkg.name} div Tab.Container | ||
$ npx jsx-info div Tab.Container | ||
# See lines where className prop was used on div component | ||
$ npx jsx-info --report lines --prop className div | ||
# See lines where \`id\` prop was used on any component | ||
$ npx jsx-info --report lines --prop id | ||
# See lines where kind prop was used with value "primary" on Button component | ||
$ npx jsx-info --report lines --prop kind=primary Button | ||
# Ignore any folder named at any depth named \`__test__\`, | ||
# as well as \`packages/legacy\` | ||
$ ${pkg.name} --ignore '**/__test__' --ignore packages/legacy | ||
$ npx jsx-info --ignore '**/__test__' --ignore packages/legacy | ||
# Enable Babel plugins | ||
$ ${ | ||
pkg.name | ||
} --add-babel-plugin decorators-legacy --add-babel-plugin pipelineOperator | ||
$ npx jsx-info --add-babel-plugin decorators-legacy --add-babel-plugin pipelineOperator | ||
Documentation can be found at https://github.com/wavebeem/jsx-info | ||
# Example .jsx-info.json config file | ||
{ | ||
"babelPlugins": ["decorators-legacy", "pipelineOperator"], | ||
"directory": "src", | ||
"ignore": ["**/__test__", "legacy/**"], | ||
"files": ["**/*.{js,jsx,tsx}"] | ||
} | ||
# Find <Button kind="primary"> | ||
$ npx jsx-info --report lines --prop kind=primary Button | ||
# Find all uses of the prop \`id\` | ||
$ npx jsx-info --report lines --prop id | ||
Full documentation can be found at https://github.com/wavebeem/jsx-info | ||
`); | ||
@@ -83,10 +110,46 @@ }); | ||
function getConfig() { | ||
if (!program.config) { | ||
return {}; | ||
} | ||
try { | ||
const explorer = cosmiconfig("jsx-info", { | ||
searchPlaces: ["package.json", ".jsx-info.json"] | ||
}); | ||
const result = explorer.searchSync(); | ||
if (result) { | ||
print(`Loaded configuration from ${result.filepath}\n`); | ||
if (result.config.directory) { | ||
result.config.directory = path.resolve( | ||
result.filepath, | ||
"..", | ||
result.config.directory | ||
); | ||
} | ||
return result.config; | ||
} | ||
} catch (err) { | ||
printError("failed to parse config file"); | ||
} | ||
return {}; | ||
} | ||
const config = getConfig(); | ||
function concat(a, b) { | ||
return [...(a || []), ...(b || [])]; | ||
} | ||
exports.prop = program.prop; | ||
exports.components = program.args; | ||
exports.showProgress = program.progress; | ||
exports.babelPlugins = program.addBabelPlugin; | ||
exports.directory = program.directory; | ||
exports.babelPlugins = concat(config.babelPlugins, program.addBabelPlugin); | ||
exports.directory = program.directory || config.directory; | ||
exports.gitignore = program.gitignore; | ||
exports.ignore = program.ignore || []; | ||
exports.files = program.files; | ||
exports.ignore = concat(program.ignore, config.ignore); | ||
exports.files = concat(program.files, config.files); | ||
if (exports.files.length === 0) { | ||
exports.files = ["**/*.{js,jsx,tsx}"]; | ||
} | ||
exports.sort = program.sort || "usage"; | ||
exports.report = program.report || ["usage", "props"]; |
@@ -7,3 +7,3 @@ const globby = require("globby"); | ||
const searchForFiles = async ({ patterns, gitignore, directory, ignore }) => { | ||
return await globby(patterns, { | ||
return await globby(patterns || "**/*.{js,jsx,tsx}", { | ||
absolute: true, | ||
@@ -10,0 +10,0 @@ onlyFiles: true, |
@@ -10,5 +10,5 @@ const { | ||
babelPlugins, | ||
ignore | ||
ignore, | ||
prop | ||
} = require("./cli"); | ||
const sleep = require("./sleep"); | ||
const parse = require("./parse"); | ||
@@ -18,4 +18,14 @@ const Reporter = require("./reporter"); | ||
const codeSource = require("./code-source"); | ||
const { formatPrettyCode } = require("./formatPrettyCode"); | ||
async function sleep() { | ||
return new Promise(resolve => { | ||
setImmediate(resolve); | ||
}); | ||
} | ||
async function main() { | ||
if (!prop && report.includes("lines")) { | ||
throw new Error("`--prop` argument required for `lines` report"); | ||
} | ||
const timeStart = Date.now(); | ||
@@ -38,11 +48,49 @@ if (showProgress) { | ||
// spinner library assumes the event loop will be ticking periodically | ||
await sleep(0); | ||
await sleep(); | ||
} | ||
try { | ||
parse(codeSource.codeFromFile(filename), { | ||
const code = codeSource.codeFromFile(filename); | ||
parse(code, { | ||
babelPlugins, | ||
typescript: filename.endsWith(".tsx") || filename.endsWith(".ts"), | ||
onlyComponents: components, | ||
onComponent: component => reporter.addComponent(component), | ||
onProp: (component, prop) => reporter.addProp(component, prop) | ||
onComponent: componentName => { | ||
reporter.addComponent(componentName); | ||
}, | ||
onProp: ({ | ||
componentName, | ||
propName, | ||
propCode, | ||
startLoc, | ||
endLoc, | ||
propValue | ||
}) => { | ||
if (prop) { | ||
let wantPropKey = prop; | ||
let wantPropValue = undefined; | ||
if (prop.includes("=")) { | ||
const index = prop.indexOf("="); | ||
const key = prop.slice(0, index); | ||
const val = prop.slice(index + 1); | ||
wantPropKey = key; | ||
wantPropValue = val; | ||
} | ||
if (propName !== wantPropKey) { | ||
return; | ||
} | ||
if (wantPropValue !== undefined && propValue !== wantPropValue) { | ||
return; | ||
} | ||
} | ||
const prettyCode = formatPrettyCode(code, startLoc.line, endLoc.line); | ||
reporter.addProp({ | ||
componentName, | ||
propName, | ||
propCode, | ||
startLoc, | ||
endLoc, | ||
prettyCode, | ||
filename | ||
}); | ||
} | ||
}); | ||
@@ -62,5 +110,3 @@ } catch (error) { | ||
printer.print( | ||
printer.styleHeading( | ||
`Scanned ${filenames.length} files in ${totalTime.toFixed(1)} seconds` | ||
) | ||
`Scanned ${filenames.length} files in ${totalTime.toFixed(1)} seconds` | ||
); | ||
@@ -73,2 +119,5 @@ if (report.includes("usage")) { | ||
} | ||
if (report.includes("lines")) { | ||
reporter.reportLinesUsage(); | ||
} | ||
reporter.reportErrors(); | ||
@@ -78,5 +127,10 @@ } | ||
main().catch(err => { | ||
// eslint-disable-next-line no-console | ||
console.error(err); | ||
if (process.env.DEBUG === "true") { | ||
// eslint-disable-next-line no-console | ||
console.error(err); | ||
} else { | ||
// eslint-disable-next-line no-console | ||
console.error(err.message); | ||
} | ||
process.exit(1); | ||
}); |
const parser = require("@babel/parser"); | ||
const traverse = require("@babel/traverse").default; | ||
const { formatPropValue } = require("./formatPropValue"); | ||
function createProp(attributeNode) { | ||
@@ -56,3 +58,5 @@ function getAttributeName(attributeNode) { | ||
function doReportComponent(component) { | ||
if (onlyComponents.length === 0) return true; | ||
if (onlyComponents.length === 0) { | ||
return true; | ||
} | ||
return onlyComponents.indexOf(component) !== -1; | ||
@@ -75,7 +79,19 @@ } | ||
const node = path.node; | ||
const component = createComponent(node); | ||
if (doReportComponent(component)) { | ||
onComponent(component); | ||
const componentName = createComponent(node); | ||
if (doReportComponent(componentName)) { | ||
onComponent(componentName); | ||
for (const propNode of node.openingElement.attributes) { | ||
onProp(component, createProp(propNode)); | ||
const propCode = code.slice(propNode.start, propNode.end); | ||
const propName = createProp(propNode); | ||
const startLoc = propNode.loc.start; | ||
const endLoc = propNode.loc.end; | ||
const propValue = formatPropValue(propNode.value); | ||
onProp({ | ||
componentName, | ||
propName, | ||
propCode, | ||
startLoc, | ||
endLoc, | ||
propValue | ||
}); | ||
} | ||
@@ -82,0 +98,0 @@ } |
@@ -40,2 +40,6 @@ const chalk = require("chalk"); | ||
const styleLinenos = (...args) => { | ||
return chalk.bold(...args); | ||
}; | ||
const spinner = ora(); | ||
@@ -46,4 +50,8 @@ | ||
// eslint-disable-next-line no-console | ||
const printError = console.error.bind(console); | ||
exports.styleComponentName = styleComponentName; | ||
exports.stylePropName = stylePropName; | ||
exports.styleLinenos = styleLinenos; | ||
exports.styleError = styleError; | ||
@@ -55,1 +63,2 @@ exports.styleNumber = styleNumber; | ||
exports.print = print; | ||
exports.printError = printError; |
@@ -20,10 +20,16 @@ const printer = require("./printer"); | ||
_sortMap(map) { | ||
_sortMapHelper(map) { | ||
const entries = Array.from(map.entries()); | ||
const sortTypes = { | ||
usage: (a, b) => b[1] - a[1], | ||
alphabatical: (a, b) => { | ||
if (b[0] < a[0]) return 1; | ||
else if (b[0] > a[0]) return -1; | ||
else return 0; | ||
usage: (a, b) => { | ||
return b[1] - a[1]; | ||
}, | ||
alphabetical: (a, b) => { | ||
if (b[0] < a[0]) { | ||
return 1; | ||
} | ||
if (b[0] > a[0]) { | ||
return -1; | ||
} | ||
return 0; | ||
} | ||
@@ -49,6 +55,8 @@ }; | ||
addProp(componentName, propName) { | ||
addProp({ componentName, propName, prettyCode, startLoc, filename }) { | ||
const props = this._componentProps.get(componentName) || new Map(); | ||
const prevPropUsage = props.get(propName) || 0; | ||
props.set(propName, prevPropUsage + 1); | ||
const prop = props.get(propName) || { usage: 0, lines: [] }; | ||
prop.usage++; | ||
prop.lines.push({ prettyCode, startLoc, filename }); | ||
props.set(propName, prop); | ||
this._componentProps.set(componentName, props); | ||
@@ -84,3 +92,5 @@ } | ||
const totalComponentsCount = this._components.size; | ||
if (!totalComponentsCount) return; | ||
if (totalComponentsCount === 0) { | ||
return; | ||
} | ||
const totalComponentUsageCount = this._sumValues(this._components); | ||
@@ -92,3 +102,3 @@ printer.print( | ||
); | ||
const pairs = this._sortMap(this._components); | ||
const pairs = this._sortMapHelper(this._components); | ||
const maxDigits = getMaxDigits(pairs.values()); | ||
@@ -105,3 +115,4 @@ for (const [componentName, count] of pairs) { | ||
reportPropUsage() { | ||
for (const [componentName, props] of this._sortMap(this._componentProps)) { | ||
const sorted = this._sortMapHelper(this._components); | ||
for (const [componentName] of sorted) { | ||
const componentUsage = this._components.get(componentName); | ||
@@ -117,8 +128,15 @@ printer.print( | ||
); | ||
const pairs = this._sortMap(props); | ||
const pairs = this._sortMapHelper( | ||
new Map( | ||
[...(this._componentProps.get(componentName) || new Map())].map(x => { | ||
x[1] = x[1].usage; | ||
return x; | ||
}) | ||
) | ||
); | ||
const maxDigits = getMaxDigits(pairs.values()); | ||
for (const [propName, count] of pairs) { | ||
for (const [propName, usage] of pairs) { | ||
printer.print( | ||
" " + printer.styleNumber(count.toString().padStart(maxDigits)), | ||
" " + printer.textMeter(componentUsage, count), | ||
" " + printer.styleNumber(usage.toString().padStart(maxDigits)), | ||
" " + printer.textMeter(componentUsage, usage), | ||
" " + printer.stylePropName(propName) | ||
@@ -128,5 +146,29 @@ ); | ||
} | ||
printer.print(` | ||
Tip: Want to see where the className prop was used on the <div> component? | ||
npx jsx-info --report lines --prop className div | ||
`); | ||
} | ||
reportLinesUsage() { | ||
// TODO: Does it make sense to sort the output here somehow? | ||
for (const [componentName, props] of this._componentProps) { | ||
for (const [, /* propName */ data] of props) { | ||
for (const lineData of data.lines) { | ||
const { filename, startLoc, prettyCode } = lineData; | ||
const { line, column } = startLoc; | ||
const styledComponentName = printer.styleComponentName(componentName); | ||
printer.print( | ||
printer.styleHeading( | ||
`${styledComponentName} ${filename}:${line}:${column}` | ||
) | ||
); | ||
printer.print(prettyCode); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
module.exports = Reporter; |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
29485
16
634
117
7
2
+ Addedcosmiconfig@^5.2.1
+ Addedargparse@1.0.10(transitive)
+ Addedcaller-callsite@2.0.0(transitive)
+ Addedcaller-path@2.0.0(transitive)
+ Addedcallsites@2.0.0(transitive)
+ Addedcosmiconfig@5.2.1(transitive)
+ Addederror-ex@1.3.2(transitive)
+ Addedesprima@4.0.1(transitive)
+ Addedimport-fresh@2.0.0(transitive)
+ Addedis-arrayish@0.2.1(transitive)
+ Addedis-directory@0.3.1(transitive)
+ Addedjs-yaml@3.14.1(transitive)
+ Addedjson-parse-better-errors@1.0.2(transitive)
+ Addedparse-json@4.0.0(transitive)
+ Addedresolve-from@3.0.0(transitive)
+ Addedsprintf-js@1.0.3(transitive)