react-scanner: static analysis that counts how your design system is actually used
Extract React components and props usage from code.
At a glance
- What is it?
- react-scanner crawls a directory, parses every file with the TypeScript ESTree parser, and extracts each rendered React component with its props into a JSON report that processors turn into usage counts. It answers the question a design system team actually has, which is which components and which prop values are still in use, and it was last pushed on 2026-04-20.
- Who is it for?
- react-scanner suits a design system or component library team that wants a usage census before removing a prop or a variant, because the raw report records import origin, prop values and source locations for every instance. It does not suit a team that needs the count enforced continuously, since the tool reports rather than blocks, and it does not suit a codebase where the components of interest are not statically imported.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Activity is slowing. The repository last received commits 6 months ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The question it answers is which props are still in use
The README frames the tool around a question a design system owner actually has to answer, and the questions are listed as a set of bullets rather than as features.
How often is a certain component used in your design system, which the count-components processor answers. How often is a certain prop in a given component used, which the count-components-and-props processor answers. And looking at some prop in a given component, what is the distribution of values used, with the parenthetical that you might consider deprecating a certain value.
That last one is the reason the tool exists. Deprecating a prop value is a decision you cannot make from the component library alone, because the library tells you what a prop can be, not what people actually pass to it. The same applies to whole components: a design system accumulates components nobody uses, and nobody notices because there is no count.
The mechanism underneath is static analysis. The tool statically analyzes the given code, TypeScript supported, and extracts React components and props usage. Nothing is executed. The tool reads your source files, finds the JSX, and reports what is written there.
That design choice has consequences. The tool sees what is in the repository, not what runs in production. A component used only in a Storybook story, a test or an example counts the same as one used in the application, and a component rendered dynamically through a variable name is invisible. The README's own example output shows a Footer with a single instance and empty props, which is the kind of entry that tells you a component is barely used at all.
The primary language is JavaScript, the licence is MIT, and the topics name the mechanism directly with abstract-syntax-tree, components, jsx, react, stats and usage.
Crawl, parse, then extract into a per-component JSON report
The pipeline is three steps, and the README describes the first two closely enough to know where the work happens.
First, it crawls the given directory and compiles a list of files to be scanned. Second, it scans every file and extracts rendered components and their props into a JSON report. The report is keyed by component name, and each entry holds an instances array where every instance records the import information, the props, whether props were spread, and the location.
The import information block has three fields: imported, local and moduleName. The distinction matters and the example shows why. A component imported under an alias records the imported name, the local name it is bound to, and the module it came from. So an aliased import is still attributable to the original component, which is the only way a usage count means anything once a codebase uses renaming conventions.
The props block records each prop and its value as written, with a string literal recorded as its literal text and an identifier recorded as a marker. The marker convention for an identifier value is what makes the report usable, because a literal can be counted and grouped but an identifier cannot be resolved without running the code, and pretending otherwise would be misleading.
The propsSpread boolean is the field that tells you when a count is unreliable. If a component is rendered with a spread, the tool cannot know which props actually reach it, and it records that fact instead of guessing.
The location block gives the file and the start line and column of the instance. With that, the report is actionable rather than merely descriptive: you can go from a prop value you intend to deprecate straight to the line that uses it.
The parser choice is visible in the dependency list. @typescript-eslint/typescript-estree at 8.8.0 does the parsing, which is what gives the TypeScript support, and typescript itself is pinned at 5.6.2. fdir handles the directory crawl, picomatch handles the include and exclude patterns, dlv and dset handle nested object access and assignment for the report structure, is-plain-object validates the config, and astray with escodegen-wallaby in the dev dependencies is there for generating code from the AST in tests.
Everything is driven by one config file
The README is blunt about the control surface: everything that react-scanner does is controlled by a config file. The config file can be located anywhere and it must export an object.
module.exports = {
crawlFrom: "./src",
includeSubComponents: true,
importedFrom: "basis",
};Three options carry the whole workflow. crawlFrom is the directory the crawl starts from, absolute or relative to the config file location. includeSubComponents decides whether components nested inside other components are included. And importedFrom restricts the scan to components coming from a particular package, which is the setting that separates a whole-codebase census from a single design system census.
That last option is the one to think about carefully. The example uses basis as the package name and the sample output reports BasisProvider, Container, Text and Link, all from that module. Without it, the tool would report every React component in your application, including your own feature components, and the counts would be a different question.
The config file also sets rootDir, the path to the root directory of the project, which defaults to the config directory when a config file is used, and exclude, which accepts an array of strings or regexes matched against directory names during the crawl. The options table is truncated in the README at that point, so the remaining options are not documented in the visible text and have to be read from the source or the type definitions.
The config is not limited to a file. The tool can also be run programmatically, in which case the config options are passed directly to the run function.
import scanner from "react-scanner";
const output = await scanner.run(config);That matters for anyone who wants the report inside a larger tool, a script or a CI job, rather than printed to a terminal by the CLI.
Install and run it in three commands
The installation is a single dev dependency, which is the right shape for a tool you run deliberately rather than one you ship.
npm install --save-dev react-scannerThen you run it against a config file.
npx react-scanner -c /path/to/react-scanner.config.jsWith the example config above, the console output is a per-component summary with an instance count and a prop histogram.
{
"Text": {
"instances": 17,
"props": {
"margin": 6,
"color": 4,
"textStyle": 1
}
},
"Button": {
"instances": 10,
"props": {
"width": 10,
"variant": 5,
"type": 3
}
},
"Footer": {
"instances": 1,
"props": {}
}
}Reading that output is where the tool earns its place. Text with seventeen instances is a heavily used component, Button with ten is moderate, and Footer with one instance and no props at all is a candidate for removal. The prop histograms are the more interesting part, because a prop value used once is a value you can deprecate without much friction, and a prop used ten times is a different conversation.
The raw report is the intermediate form, and the README is explicit that it is used to generate something that is useful to you. Once you have the result you are interested in, you can write it to a file or simply log it to the console. So the processors are the output shape and the report is the input you can take further.
The package metadata gives the runtime floor: node >=14.x, and the CLI entry point is bin/react-scanner with the library entry at src/scanner.js.
The build is a code generator, and the tests demand full coverage
Two scripts in the package manifest explain how the processor side of the tool is built, and they are unusual enough to be worth naming.
The build script is not a bundler. It is node scripts/processors.js, which means the processors are themselves generated code rather than hand-written source. The publish hook runs that build, prepublishOnly pointing at npm run build, so what lands on npm is the generated output.
The test script is uvu src test, and the coverage script is the more revealing one: c8 restricted to the src tree, writing to coverage, with check-coverage and branches at 100, functions at 100, lines at 100 and statements at 100.
Hitting a hundred percent on every metric is an unusual and demanding target for a project of this size. It says the author treats the scanner as logic that must be fully exercised, which is the right standard for a tool whose output people use to decide whether to delete a public API.
The rest of the tooling is conventional and modern. ESLint with max-warnings 0 across all JavaScript, Prettier for formatting JavaScript, JSON and Markdown, husky for git hooks with a prepare script that installs them, and a .husky/ directory at the root. renovate.json is present, so dependency updates are automated. The test runner for watch mode is watchlist, and there is an .nvmrc pinning a Node version for development.
One inconsistency is worth noting for anyone pinning versions. The release history lists 1.0.0 from 2021-05-02 as the most recent GitHub release, while the package manifest declares version 1.2.0. The published npm version and the tagged release are not the same number, so the tag list understates where the package actually is. The last push was on 2026-04-20, which is close to six months before the present date, and the repository is not archived.
Where react-scanner is the wrong tool
The first wrong-tool case is a team that wants the count enforced rather than reported. The tool produces a census. It does not fail a build, block a pull request or warn an author who passes a deprecated value. A lint rule that forbids the prop value is the different approach: it operates at authoring time, on the change, and it can only ever catch values in code someone is editing now. The scanner operates on the whole repository and can tell you that a value is used in forty places, including in files that have not been touched in a year. If your problem is preventing new usage, a lint rule is the cheaper and more reliable tool, and this scanner is the wrong one. If your problem is finding out what already exists before you delete an API, the scanner is the right one and the lint rule cannot help, because a lint rule never looks at history.
The second case is a codebase that renders components dynamically. Static extraction finds JSX with a component name in it. A component referenced through a variable, chosen from a map at runtime, or produced by a factory, is not extracted, and neither is a component whose name comes from a prop. A design system used mainly through a dynamic registry will report low counts for components that are in fact widely used, and the report will mislead you in the dangerous direction, towards removing something that matters.
The third case is a monorepo where the design system is consumed from another package and the component names differ from the local ones. The importedFrom filter matches on the module the component is imported from, and the local alias is recorded separately, so a package that re-exports components under different names will attribute instances to the wrong key unless you know how the re-exports are named.
The fourth case is expecting runtime truth. The tool reads source. It does not know which code paths execute, what a feature flag disables, or which bundle is actually deployed to users. For a component library that is mostly static, source is a good proxy. For an application with heavy conditional rendering, the static count is an upper bound on real usage rather than a measurement of it.
The last limitation is about the tool itself rather than your code. The last push was on 2026-04-20, and the most recent GitHub release is 1.0.0 from 2021-05-02. The package manifest is at 1.2.0, the dependency on typescript is pinned at 5.6.2 and the parser at 8.8.0, and none of that is floating. You can run it today, and the pinned dependency set means the behaviour will not change under you, but you are running a tool whose published release history stopped in 2021.
Editorial conclusion
react-scanner suits a design system or component library team that wants a usage census before removing a prop or a variant, because the raw report records import origin, prop values and source locations for every instance. It does not suit a team that needs the count enforced continuously, since the tool reports rather than blocks, and it does not suit a codebase where the components of interest are not statically imported. Before adopting, run it with crawlFrom pointed at your source root and importedFrom set to your package name, then read the report for a component you are about to deprecate and confirm by hand that a zero count is not hiding a spread or a dynamic component reference.
Frequently asked questions
What does react-scanner do?
It statically analyzes a codebase, TypeScript supported, and extracts rendered React components and their props into a JSON report. The report records each instance with its import origin, prop values, whether props were spread, and the file and line where it appears.
How do I install and run react-scanner?
Install it with npm install --save-dev react-scanner, then run npx react-scanner -c /path/to/react-scanner.config.js. The config file must export an object and can be located anywhere in the project.
What config options does react-scanner have?
The documented ones are rootDir, the path to the project root, which defaults to the config directory, crawlFrom, the directory the crawl starts from, and exclude, an array of strings or regexes matched against directory names. The example config also shows includeSubComponents and importedFrom.
Can react-scanner be used programmatically?
Yes. Import the scanner and pass the config options directly to the run function, for example import scanner from "react-scanner" followed by const output = await scanner.run(config).
What license is react-scanner released under?
The project is MIT licensed, with the LICENSE file at the root of the repository. The package manifest declares version 1.2.0, the engines field requires Node 14.x or newer, and the last push to the repository was on 2026-04-20.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/moroshko-react-scanner)Was this useful?