edge-compat-check
v1.0.0
Published
Static-analysis dependency scanner for edge runtime compatibility — scans your Node.js dependencies for Node.js APIs that break on Vercel Edge, Cloudflare Workers, Netlify Edge, and Fastly Compute, using per-target profiles you can run locally or as a CI
Maintainers
Readme
edge-compat-check
Static-analysis dependency scanner for Node.js APIs that break on edge runtimes.
Developed and open-sourced by Nextbridge.
edge-compat-check is a static analysis scanner that checks whether your dependencies use Node.js APIs unavailable on Vercel Edge, Cloudflare Workers, Netlify Edge Functions, and Fastly Compute — reporting what breaks, why, and which Web API to use instead. With --ci, it exits with code 1 if incompatible APIs are found, failing the pipeline before a broken deploy ships.
Contents
- Quick Start
- Features
- Why & Who It's For
- Supported runtimes
- Supported package managers
- Installation
- Usage
- CLI options
- Example output
- JSON output
- Exit codes
- CI integration
- Programmatic API
- TypeScript support
- How it works
- Limitations
- Troubleshooting
- FAQ
- Changelog
- Contributing
- License
Quick Start
npx edge-compat-checkNo install required — see Example output for a full run, or jump to CI integration to gate a pipeline on the result.
Features
- Per-target compatibility profiles —
vercel-edge,cloudflare-workers,netlify-edge,fastly-compute,strict. - Direct or deep scanning — full transitive tree via lockfile (npm, pnpm, Yarn Classic).
- CI-ready —
--ciexits non-zero when an incompatible API is found. - Machine-readable output —
--jsonfor custom tooling and dashboards. - Fast — concurrent scanning (8 workers) across large dependency trees.
- Configurable —
--ignoreto suppress specific Node core modules,--include-devto scan devDependencies. - Safe by construction — path-traversal protection and symlink-safe file discovery, with support for pnpm's linked
node_moduleslayout (pnpm's deep lockfile scanning itself remains best-effort — see Limitations). - Broad file coverage — scans
.js,.mjs,.cjs,.jsx,.ts,.mts,.cts,.tsxsource. - TypeScript-aware —
import typeand type-onlyexport type ... fromre-exports are ignored, so they don't produce false-positive findings. - Bounded scans — 2 MB per-file size cap, 80-file default cap per dependency, so runs stay fast on large trees.
- Deterministic output — stable ordering, diff-friendly in CI logs.
- Zero runtime dependencies, with TypeScript declarations for the programmatic API.
Why & Who It's For
Edge runtimes are not equal — Vercel Edge supports only a small documented set of Node.js APIs, while Cloudflare Workers (with nodejs_compat) supports most of them. A single hard-coded "unsafe module" list tuned for one target produces false positives on another, or false negatives.
edge-compat-check models each target as its own profile, checked against each vendor's compatibility docs (see src/profiles.js for sources and verification dates).
Use it if you're deploying to Vercel Edge, Cloudflare Workers, Netlify Edge, or Fastly Compute; auditing a new dependency for edge-safety; gating CI with a fast, zero-dependency check; or a library author avoiding accidental Node-only imports.
Supported runtimes
| Target | Runtime | Node.js support |
| ----------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| vercel-edge (default) | V8 isolate | Small documented set (assert, async_hooks, buffer, events, util) via import only — require() unsupported |
| cloudflare-workers | V8 + nodejs_compat | Large subset (date-gated) |
| netlify-edge | Deno | Via node: prefix (required); gaps in OS/threading |
| fastly-compute | WASM (js-compute) | None (WinterTC Web APIs only) |
| strict | — | None (assume zero Node support) |
Vercel Edge: assert, async_hooks, buffer, events, and util are supported when loaded via import (with or without the node: prefix). Every other Node core module is unsupported. A supported module still fails when loaded via require(), since Vercel Edge requires ES modules.
Netlify Edge: Node built-ins must be imported with the node: prefix (e.g. import "node:crypto"). A bare specifier (import "crypto") fails, with a suggestion to add the prefix, even for an otherwise-supported module. require() of a Node built-in is unsupported regardless of prefix.
Supported package managers
--deep resolves the full transitive tree from whichever lockfile is present:
| Package manager | Lockfile | Deep-scan support |
| --------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| npm | package-lock.json | Full — v6 dependencies tree and v7+ packages map |
| Yarn Classic | yarn.lock | Full — walks the installed node_modules tree, so root, nested, scoped, and duplicate physical installations of a locked package are all discovered and scanned |
| pnpm | pnpm-lock.yaml | Best-effort lockfile parsing (regex, not a full YAML parse; tested against pnpm lockfileVersion 5-9) — every matching physical .pnpm store directory is expanded and scanned, so distinct installed versions of the same package are scanned separately |
| Yarn Berry | yarn.lock (Berry) | May be incomplete — Yarn Classic format is what's fully supported |
| Bun | bun.lockb | Not supported — --deep falls back to a direct (non-transitive) scan and emits a warnings entry |
| Yarn PnP | .pnp.cjs/.pnp.js | Not supported — packages aren't extracted to node_modules, so most can't be resolved; emits a warnings entry |
When a deep scan finds more than one physical installation of a dependency (e.g. two pnpm-resolved versions, or a nested Yarn Classic copy), each installation is reported as its own result carrying its version and location — see JSON output.
Without --deep (or if no lockfile is found), the scanner reads direct dependencies straight from package.json — no lockfile parsing needed.
Installation
No install required — see Quick Start. For repeatable use in CI, install as a dev dependency:
npm install -D edge-compat-check
npx edge-compat-check --target vercel-edge --ciRequires Node.js >= 18. Zero runtime dependencies.
Usage
npx edge-compat-check # vercel-edge, direct deps
npx edge-compat-check --target cloudflare-workers # different runtime
npx edge-compat-check ./apps/web --target netlify-edge # subdirectory
npx edge-compat-check --deep # transitive tree
npx edge-compat-check --ci # exit 1 on failure
npx edge-compat-check --json > report.json # machine-readable
npx edge-compat-check --ignore crypto,fs # suppress modulesCLI options
| Flag | Description |
| -------------------- | ------------------------------------------------------ |
| [path] | Project directory to scan (default: current directory) |
| --target <name> | Compatibility profile (default: vercel-edge) |
| --deep | Scan full transitive tree via lockfile |
| --include-dev | Include devDependencies |
| --ignore <modules> | Comma-separated modules to skip |
| --ci | Exit code 1 if incompatible APIs found |
| --json | JSON output |
| --no-suggestions | Hide suggestions |
| --no-color | Disable colors |
| -v, --version | Show version |
| -h, --help | Show help |
Unknown flags exit with code 2 so misspelled CI commands fail fast.
Note:
--ignoresuppresses findings for the specified module across the entire scan. Use it only for confirmed false positives, not to hide genuine compatibility issues.
Example output
A run with one incompatible dependency:
npx edge-compat-check v1.0.0 · 3 deps · direct
target: Vercel Edge Runtime (Middleware)
✗ some-pkg
imports "crypto"
"crypto" is not supported on Vercel Edge Runtime (Middleware).
→ Use the Web Crypto API (globalThis.crypto / crypto.subtle).
Result: 1 dep(s) may break on Vercel Edge Runtime (Middleware)A clean run:
npx edge-compat-check v1.0.0 · 12 deps · direct
target: Vercel Edge Runtime (Middleware)
✓ No incompatible APIs found for this target.
Result: PASSA dependency that hits the file scan cap is flagged as a warning, not a failure — see Limitations:
⚠ some-large-pkg
[Warning] Exceeded the 80 file limit. Some files were not scanned.JSON output
--json writes a stable, machine-readable object. This is the actual output from scanning this dependency-free package:
{
"version": "1.0.0",
"timestamp": "2026-07-15T08:31:25.639Z",
"target": "Vercel Edge Runtime (Middleware)",
"mode": "direct",
"warnings": [],
"totalDeps": 0,
"scanned": 0,
"flagged": [],
"skipped": []
}version and location are optional and only present when the scanner can attach an identity to the result — most commonly on a --deep scan where pnpm or Yarn Classic resolved more than one physical installation of the same dependency (see Supported package managers). location is a repository-relative path to the physical package directory that produced the result, so duplicate installations of the same dependency name are distinguishable in both flagged and skipped.
mode is one of "direct", "deep" (lockfile resolved successfully), or "direct-no-lockfile" (--deep was requested but no supported lockfile was found, so the scan fell back to direct dependencies).
warnings is a top-level array of strings, always present (empty when there's nothing to report). It's populated for things like an unrecognized pnpm-lock.yaml version, a corrupt package-lock.json, or the Bun/Yarn PnP fallbacks described in Supported package managers.
skipped[].reason is one of four values:
| Reason | Meaning |
| ------------------- | -------------------------------------------------------------------------------------- |
| notInstalled | Dependency isn't present in node_modules |
| unresolvedLayout | Deep scan couldn't map the dependency to a location on disk |
| workspaceSkipped | A workspace/link: reference couldn't be resolved |
| boundaryViolation | Resolving the dependency would follow a symlink outside the project/workspace boundary |
Note: an entry appears in
flaggedif it hasfindingsor ifhitLimitistrue— those aren't the same thing.hitLimit: trueonly means the 80-file scan cap was reached before the whole package could be scanned; it is not itself a compatibility failure. The CLI's own--ciexit code correctly only fails on realfindings. If you build pass/fail logic on top of this JSON, filter onfindings.length > 0, not just presence inflagged.
Exit codes
| Code | Meaning |
| ---- | --------------------------------------------------------------------------------- |
| 0 | Scan completed. Also used when findings exist unless --ci is set. |
| 1 | --ci was set and one or more dependencies had incompatible API findings. |
| 2 | Invalid arguments, unknown flags, unknown target profile, or package read errors. |
CI integration
name: edge-compat
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx edge-compat-check --target vercel-edge --deep --ciProgrammatic API
import { scanProject, formatReport } from "edge-compat-check";
const scan = await scanProject("./", {
profile: "cloudflare-workers",
deep: true,
ignore: new Set(["crypto"]),
});
console.log(formatReport(scan));scanProject(dir, opts) → Promise<ScanResult>
| Option | Type | Default | Description |
| ------------- | ------------- | -------------------------------------- | -------------------------------------------- |
| profile | string | "vercel-edge" | Target profile |
| deep | boolean | false | Scan transitive tree |
| includeDev | boolean | false | Include devDependencies |
| filesPerDep | number | 80 | Max files to scan per dependency |
| ignore | Set<string> | — | Modules to skip |
| boundary | string | the project's node_modules directory | Boundary path for lockfile/symlink traversal |
| onProgress | function | — | (depName, done, total) => void |
formatReport(scan, opts) → string
Renders a ScanResult as the same human-readable report the CLI prints. opts.showSuggestions (default true) and opts.color (default true) mirror the CLI's --no-suggestions / --no-color flags.
toJSON(scan) → string
Renders a ScanResult as the same JSON string the CLI prints with --json — see JSON output for the schema.
TypeScript support
Type declarations ship in src/index.d.ts and are resolved automatically via the package's exports map — no @types package needed.
import type {
ScanResult,
DependencyResult,
Finding,
ScanProjectOptions,
} from "edge-compat-check";| Export | Kind | Notes |
| ---------------------------------------------------------------------------- | ------------------------- | --------------------------------------------------------- |
| scanProject | function | See Programmatic API |
| formatReport | function | See Programmatic API |
| toJSON | function | See Programmatic API |
| PROFILES | Record<string, Profile> | The built-in compatibility profiles, keyed by target name |
| NODE_CORE_MODULES | string[] | Every Node.js core module name the scanner recognizes |
| Finding, DependencyResult, ScanResult, ScanProjectOptions, Profile | types | Public shapes for the API above |
Note: a
FindingfromscanProject()has asuggestfield. The CLI's--jsonoutput (andtoJSON()) renames it tosuggestionfor readability — see the schema in JSON output. If you consumescanProject()directly rather thantoJSON(), readfinding.suggest, notfinding.suggestion.
Note:
DependencyResulthas optionalversionandlocationfields, populated the same way as the JSON output'sflagged[]/skipped[]entries — see JSON output.
How It Works
- Collects dependency names from
package.json(direct) or a lockfile (transitive, via--deep— see Supported package managers). - Statically scans each installed package for imports/requires of Node core modules.
- Checks each import against the target's compatibility profile (see Supported runtimes).
- Reports findings with reasons and suggested Web API alternatives.
- In
--cimode, exits with code 1 if an incompatible API is found — so the build fails before the deploy does.
Limitations
- Lexical, not AST-based. No code is executed and no JavaScript/TypeScript parser is used; guarded imports (e.g. behind a runtime check) may be false positives, and dynamically constructed import specifiers (e.g. built from a variable) can't be detected.
- Conditional exports and the reachable import graph aren't resolved. The scanner reads every scanned source file's imports rather than following
package.jsonexportsconditions or which files are actually reachable at runtime, so an inactive platform-specific file can still produce a false positive. - 80-file default scan cap per dependency (
filesPerDep, configurable via the programmatic API). See thehitLimitnote in JSON output. - Lockfile support varies by package manager — see Supported package managers.
- Profiles drift. Edge runtimes evolve fast; treat results as a signal, not a guarantee. See
src/profiles.jsfor verification dates and sources. - Requires
node_modules. Run afternpm ci/pnpm install/yarn install. - Nested template literals in source may cause rare false positives/negatives during comment/string stripping.
Troubleshooting
It says "No incompatible APIs found" but I expected findings.
Confirm the dependency was actually scanned, not skipped — see skipped[].reason in JSON output. The import may also be behind a runtime guard (static analysis can't evaluate conditions), or the dependency may have hit the scan cap (hitLimit: true).
A command exits with code 2 — what does that mean?
Code 2 means invalid usage: an unknown flag, a missing/unknown --target profile, or a package.json that couldn't be read. It's not a compatibility failure — check the flag spelling against --help.
--ci didn't fail the build even though hitLimit was true.
That's expected — see the note in JSON output. Only real findings fail --ci; a scan cap warning alone does not.
FAQ
Does this replace testing on the actual edge runtime? No — it's a static, import-based pre-flight signal, not a substitute for a real deploy or preview build.
Which package managers are supported? See Supported package managers for the full breakdown by lockfile.
Can I scan a subdirectory in a monorepo?
Yes — pass the path as the first argument, e.g. edge-compat-check ./apps/web --target cloudflare-workers.
Does it work with TypeScript projects?
Yes — .ts, .mts, .cts, and .tsx source files are scanned like any other, and import type / type-only export type ... from declarations are ignored so they don't produce false positives. The programmatic API also ships its own type declarations; see TypeScript support.
Can I use this outside the CLI, e.g. in a custom script or dashboard?
Yes — see Programmatic API for scanProject, formatReport, and toJSON.
Changelog
Initial release. See Features above.
License
Built and maintained by Nextbridge
