npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@m9ter1a/freshclone

v0.2.1

Published

Will this repo build on someone else's clean machine? Audit a project for hidden dependencies on your local environment.

Readme

freshclone

CI npm

freshclone finds the things in your repository that only work because of how your machine is set up. A script that calls a globally installed binary. An import whose case is wrong in a way only Linux notices. A file the build needs that .gitignore keeps out of the clone. One command, one score, one list of what to fix.

Install

npx @m9ter1a/freshclone ./

Nothing to install for a one-off run. To keep it in a project:

npm i -D @m9ter1a/freshclone

Node 18 or newer. One dependency.

What it looks like

  freshclone  my-app

  Grade C   70/100

  Dependencies    ████████░░  75
  Portability     ████████░░  75
  Reproducibility █████░░░░░  50
  Environment     ████████░░  84

  4 errors  ·  2 warnings  ·  1 info

  ERR   `./Renderer.js` resolves to `src/renderer.js`, which is spelled differently  src/index.js:1
        The import asks for `src/Renderer.js` but the file on disk is `src/renderer.js`. Your
        filesystem ignores the difference; ext4 on a Linux CI runner does not, and the build
        fails there with a module-not-found error.
        fix: Rename the file or fix the specifier so both use the same case.
        [path-case-mismatch]

  ERR   `tsc` is called by scripts.build, but no dependency provides it  package.json:6
        None of the declared dependencies ship a binary by that name, so after a clean
        `npm ci` the command is simply not on PATH. It most likely works for you because it
        is installed globally.
        fix: Add it to devDependencies (`npm i -D typescript`).
        [bin-not-declared]

  ERR   `src/config.local.js` is imported by `src/index.js` but git ignores it  src/index.js:2
        The file exists on your machine and never reaches the repository, so a fresh clone
        cannot compile this import at all.
        [gitignored-build-input]

  ...

knip checks your code. freshclone checks your environment.

knip and depcheck are import-graph analysers. They are very good at "you import a package that isn't in package.json", and freshclone does not go anywhere near that. It looks for the failures that an import graph cannot see even in principle:

| Problem | knip | freshclone | | --- | :---: | :---: | | Undeclared JS import | ✅ | — not our job | | A script calls a binary no dependency ships | ❌ | ✅ | | import './Foo' when the file is foo.ts | ❌ | ✅ | | The build reads a file that is in .gitignore | ❌ | ✅ | | The lockfile has drifted, so npm ci will fail | ❌ | ✅ | | Code reads process.env.X that is documented nowhere | ❌ | ✅ |

Use both. They do not compete.

Usage

freshclone                        # audit the current directory
freshclone ./path/to/repo
freshclone --json                 # machine-readable report
freshclone --markdown             # for a PR comment
freshclone --min-score 80         # gate a CI job
freshclone --only portability     # one category
freshclone --ignore-rule bin-not-declared

Exit codes — 0 clean · 1 an error-level finding, or the score is under --min-score · 2 freshclone could not run (no package.json, bad arguments). That makes it a CI gate out of the box:

- run: npx @m9ter1a/freshclone . --min-score 80

What you can point it at

Anything with a package.json at its root. Without one it exits 2 and tells you so, rather than guessing.

It earns its keep on:

| | | | --- | --- | | Libraries and CLIs you publish | The core case. Your node_modules has been there for months; a stranger's has not. | | Monorepos | Workspace members declared in workspaces or pnpm-workspace.yaml are audited individually: each member's own scripts and dependencies, with root hoisting and workspace:* siblings taken into account. Measured on vite — 294 members, 1697 source files, one pass. | | Apps built with Vite, Vue, Svelte, Astro or plain HTML | Script regions inside markup are read, so a mis-cased <script src> or a broken import in a <script setup> block is caught like any other. | | Anything with a CI pipeline | Exit codes and --min-score make it a gate; --markdown makes it a PR comment. |

It is thinner on:

  • pnpm, yarn and bun repositories. Everything works, but lockfile verification is by package name only; the deep range check is npm-only. The report labels it skipped instead of implying a check happened.
  • Repositories without git available. The .gitignore rule reports itself skipped rather than passing.

It is the wrong tool for: Python, Go, Rust or any non-Node ecosystem; finding unused or undeclared imports (that is knip); vulnerability scanning (npm audit); and checking whether your package is ready to publish — exports, files, tarball contents — which is a different job for a different tool.

Scale. The largest repository tested is svelte at 8137 source files; a full audit is a single pass with no network access and no install step, so it is fast enough to run on every commit.

What it checks

| Rule | Severity | What it means | | --- | --- | --- | | path-case-mismatch | error | An import differs from the real filename only in case. Fine on Windows and macOS, fatal on Linux CI. | | unresolved-relative-import | error | A relative import points at nothing in the repository. | | bin-not-declared | error | A script runs a command that no declared dependency provides — it is probably installed globally on your machine. | | system-bin-required | warn | A script needs a system tool (make, python, deno) that npm cannot install. | | npx-on-the-fly | info | A script downloads a package at run time, so the build needs network and is not reproducible. | | gitignored-build-input | error | Something the build reads is excluded by .gitignore and will not survive a clone. | | lockfile-missing | error | No lockfile is committed, so npm ci cannot run. | | lockfile-out-of-sync | error | package.json and the lockfile disagree; a clean install refuses to reconcile them. | | lockfile-multiple | warn | Two package managers' lockfiles are committed. | | lockfile-manager-mismatch | warn | packageManager names a manager with no matching lockfile. | | undocumented-env-var | warn | Code reads an environment variable that no .env.example or README mentions. | | missing-env-example | warn | .env is ignored and nothing documents what belongs in it. | | engines-node-missing / node-version-unpinned | info | Nothing states which Node version the project needs. | | engines-node-unsatisfied | warn | Your local Node is outside the range the project declares, so the supported range is untested. |

Scoring: four categories, each 0–100, combined with weights (portability and dependencies count most). A ≥ 90, B ≥ 75, C ≥ 60, D ≥ 40, F below. Repeats of the same rule stop counting after a few, so one systemic mistake cannot zero a category.

--only narrows the audit rather than filtering the output: the categories you did not ask for are dropped from the report entirely and the weights are renormalised, so an unaudited category never contributes a free 100.

What the report admits it did not read

A clean score over a repository freshclone barely opened is not good news, so every run prints what it skipped:

  0 errors  ·  0 warnings  ·  0 info
  184 of 224 source files analysed  ·  301 files in the repo
  7 nested packages folded in as part of this project

  Not analysed:
    · 1 nested package (demo/) — separate projects, audit each on its own

If the analysed share drops below 60%, the grade itself is prefixed with a Partial audit warning rather than a footnote. The same numbers are in --json under coverage, with a partial boolean.

What counts as part of your project. A nested package.json is classified, not assumed foreign. Workspace members named by workspaces or pnpm-workspace.yaml are yours. So is a subpath stub — a package.json with no scripts, dependencies or lockfile, the kind that exists so import "preact/hooks" resolves. Everything else (examples/, benchmark harnesses, e2e projects) is somebody else's audit and is reported as skipped.

Markup. Script regions of .vue, .svelte, .astro and .html are read: the template, styles and prose around them are blanked out first, so markup text is never mistaken for code and line numbers stay exact. A <script src> is case-checked but never called missing — a page routinely points at a bundle the build has yet to produce.

Remaining blind spots: .mdx (imports sit in prose beside fenced code samples), and anything past the file-walk cap.

Silencing what you meant to write

Some repositories contain broken code on purpose. A compiler's test suite has a directory named missing-file; a resolver's fixtures import ./irrelevant. Those findings are true and unwanted, so they are suppressed by path rather than argued away:

{
  "freshclone": {
    "exclude": ["packages/svelte/tests", "playground"],
    "ignoreRules": ["npx-on-the-fly"]
  }
}

A pattern with no wildcard means the directory and everything under it. * matches inside one path segment, ** across segments. The same thing ad hoc: freshclone --exclude playground --ignore-rule npx-on-the-fly.

Two things about this are deliberate.

Exclusion filters findings, never the scan. Excluded files are still read and still resolve, so an import pointing into an excluded directory keeps working. Dropping those files instead would turn every such import into a phantom unresolved-relative-import — the suppression would manufacture the errors it was meant to silence.

The count stays visible. Every run prints 37 findings suppressed: 37 in excluded paths, and --json carries the same numbers. A repository that excluded its way to an A says so out loud. A malformed config is reported too, rather than quietly doing nothing.

The design rule: silence beats guessing

What a clean run does and does not mean. The two directions are not symmetric. A finding is close to certain: if freshclone says a script calls a binary nothing declares, that command really is missing after npm ci. A clean run is weaker evidence — it means no rule found anything to report, not that the project is portable. Static analysis cannot see a missing system library, a native module that fails to compile, or anything that only shows up when the build actually runs. That is the honest ceiling of this approach; running the build in a clean container (--deep) is what would raise it.

A linter that cries wolf gets uninstalled after one run. So freshclone reports nothing whenever it cannot be sure:

  • An import written through a tsconfig alias is case-checked, but an alias that resolves to nothing is silence, not a finding: tsc already reports that, and reading paths is an approximation of the real module resolver. Bare package specifiers are skipped entirely.
  • A missing dist/… or generated/… target is not a finding — the build produces those.
  • A nested package.json marks somebody else's project. examples/, e2e/ and test fixtures are not audited as part of yours.
  • Semver ranges it cannot model exactly (hyphen ranges, prereleases, workspace:) produce no verdict at all.
  • pnpm and yarn lockfiles are checked by package name only, and the report says so out loud rather than implying a deep check happened.
  • Without git on PATH, the .gitignore rule reports itself as skipped instead of passing.

The suite runs on Linux, macOS and Windows against Node 20 and 22, and path-case-mismatch is asserted on all of them — a rule about case sensitivity that is only ever tested on a case-insensitive filesystem proves nothing. So is gitignored-build-input, which shells out to git check-ignore and therefore depends on the platform's git.

Verified against eleven real repositories — axios, chalk, clsx, defu, express, hono, p-limit, preact, svelte, vite and zod — at 100% source coverage on six of them and 82–90% on the rest.

svelte and vite score D and C out of the box because their compiler fixtures are full of deliberately broken imports. Excluding those directories — two --exclude flags each — takes svelte to B 87 and vite to A 95, and what remains is real: svelte's check:tsgo script calls a binary no package in the repository declares.

Not in v1

No Docker, no container builds (that is --deep, later). No CVE scanning — that is npm audit. No publish-readiness audit (exports, files, tarball size). No non-JS ecosystems. And, again, no import-graph analysis: use knip.

API

The CLI is a thin wrapper. Everything is available as a library:

import { audit } from "@m9ter1a/freshclone";

const report = audit("./my-repo", { only: ["portability"] });
console.log(report.grade, report.score, report.findings);

Every rule is a pure function of a ProjectContext; all filesystem and git access happens once, in scan().

License

MIT