Skip to content

Configuration Examples

Complete configuration files for common project shapes. Each one is valid as written, and each notes which parts actually change jscan's behavior today.

Remember two rules while reading these:

  • analysis.exclude_patterns replaces the default list rather than adding to it.
  • Short entries in that list also match as substrings of a file's full path, so out removes src/routes/ and src/layout/. Every example below therefore avoids the short entries. The reference explains this in full.

Starting point for any project

The smallest file worth writing. It sets complexity thresholds a little more forgiving than the built-in defaults, and it fixes the exclude list so that no source directory is dropped by accident.

jscan.config.json
{
  "complexity": {
    "low_threshold": 10,
    "medium_threshold": 20
  },
  "analysis": {
    "exclude_patterns": [
      "node_modules",
      "coverage",
      ".git",
      "*.min.js",
      "*.bundle.js",
      "*.map"
    ]
  }
}

Run it against your source directory rather than the repository root, so that build output stays out of the analysis without needing a pattern for it:

jscan analyze src/

React or Next.js application

Next.js projects keep generated output in .next and often have a src/app or src/pages tree full of route files. The route directories are exactly the ones the default exclude list damages, so the custom list matters here.

jscan.config.json
{
  "complexity": {
    "low_threshold": 12,
    "medium_threshold": 24
  },
  "output": {
    "min_complexity": 3
  },
  "analysis": {
    "exclude_patterns": [
      "node_modules",
      ".next",
      ".vercel",
      ".turbo",
      "coverage",
      ".git",
      "*.min.js",
      "*.bundle.js",
      "*.map"
    ]
  }
}

The thresholds are raised because component code accumulates conditional rendering, which counts toward cyclomatic complexity without being genuinely hard to read. min_complexity of 3 hides the trivial components so that the report is about the parts worth looking at.

Next.js reserves several export names that nothing in your code imports. jscan recognizes them and does not report them as unused, but only inside App Router convention files, meaning a file under a path containing /app/ and named page, layout, template, loading, error, not-found, default, or route. In those files the default export is exempt, along with metadata, generateMetadata, viewport, generateViewport, generateStaticParams, dynamic, dynamicParams, revalidate, fetchCache, runtime, preferredRegion, and maxDuration. In route files the HTTP verb exports such as GET and POST are exempt as well.

This exemption is easy to lose

A file named layout.tsx contains the letters out, so the default exclude_patterns drops it before the exemption is ever consulted. The custom list above avoids that, which is another reason not to keep the default list in a Next.js project.

Node.js backend service

Backend code is a better fit for stricter thresholds, and the express-style routes directory needs the same care as above.

jscan.config.json
{
  "complexity": {
    "low_threshold": 8,
    "medium_threshold": 15,
    "max_complexity": 20
  },
  "analysis": {
    "exclude_patterns": [
      "node_modules",
      "coverage",
      ".git",
      "*.min.js",
      "*.map"
    ]
  }
}

max_complexity is read only by jscan check, where it supplies the default for --max-complexity. With this file in place, the gate becomes:

jscan check src/          # Fails above complexity 20

Library or published package

A library's public exports are consumed by other repositories, so jscan will always report them as unused. The gate has to allow dead code, which makes the configuration file itself fairly plain.

jscan.config.json
{
  "complexity": {
    "low_threshold": 8,
    "medium_threshold": 16,
    "max_complexity": 20
  },
  "analysis": {
    "exclude_patterns": [
      "node_modules",
      "coverage",
      ".git",
      "*.map"
    ]
  }
}
# The unused-export warnings are expected here
jscan check --allow-dead-code src/

You still get value from the dead code analysis in jscan analyze, where the critical findings, which are genuinely unreachable statements, are worth acting on even though the warnings are not.

Monorepo

There is no workspace-aware mode. Run jscan once per package, and give each package its own file so that thresholds can differ between a strict core library and a looser internal tool.

repo/
├── jscan.config.json          ← fallback for packages without their own
└── packages/
    ├── core/
    │   ├── jscan.config.json  ← stricter
    │   └── src/
    └── web/
        ├── jscan.config.json  ← looser
        └── src/

Because discovery walks upward from the analyzed path, jscan analyze packages/core/src finds packages/core/jscan.config.json first and falls back to the repository root file only when the package has none.

# Analyze each package separately
for pkg in packages/*/; do
  echo "== $pkg"
  jscan check "$pkg/src" || exit 1
done

Analyzing packages separately has one consequence worth understanding. The unused-export check can only see the files in the current run, so anything packages/web imports from packages/core is reported as an unused export while core is analyzed alone. Run jscan analyze packages/ to see the whole picture, and the per-package runs to gate each package.

Legacy codebase you are improving gradually

When the current state is far from where you want it, set thresholds you can actually pass today and tighten them over time.

jscan.config.json
{
  "complexity": {
    "low_threshold": 20,
    "medium_threshold": 40,
    "max_complexity": 60
  },
  "output": {
    "min_complexity": 15
  },
  "analysis": {
    "exclude_patterns": [
      "node_modules",
      "coverage",
      ".git",
      "legacy/generated",
      "*.min.js",
      "*.map"
    ]
  }
}

The high min_complexity keeps the report focused on the worst functions rather than producing thousands of lines nobody reads. Lower max_complexity by five every time the build passes comfortably, and the gate will ratchet the codebase in the right direction without ever blocking work.

Note that legacy/generated is long enough not to over-match, which is what makes it safe to include. Prefer specific multi-segment paths over short names for this reason.

YAML instead of JSON

The loader accepts YAML when the filename ends in .yaml or .yml. The keys are identical.

jscan.yaml
complexity:
  low_threshold: 10
  medium_threshold: 20
  max_complexity: 25

output:
  min_complexity: 2

analysis:
  exclude_patterns:
    - node_modules
    - coverage
    - .git
    - "*.min.js"
    - "*.map"

See also