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

layero-detection

v0.1.7

Published

Layero's unified framework / build-command / output / runtime detection — one declarative spec + one algorithm (Snapshot → BuildPlan). The single source of truth shared by the Layero CLI and control-plane.

Readme

layero-detection

Given a snapshot of a project — package.json, the files in the root, config files, requirements.txt — works out what it is and how to build it: the framework, the build command, the output directory, whether it needs a long-lived process, and where the pieces of a fullstack repository live.

import { detect } from "layero-detection";

const plan = detect(snapshot);
// → { projectKind: "static" | "spa" | "ssr" | "runtime" | "fullstack",
//     units: [{ role, framework, buildCmd, outputDir, startCmd, runtimeKind, … }],
//     … }

snapshot is what you read off disk: packageJson, sets of files and dirs, the text of any config files you found, requirementsTxt, hasHtml and an optional layeroJson. Nothing is read for you — the package is pure and has no I/O, which is what lets the same algorithm run in a CLI, a server and a build container.

Vite, Next.js, Astro, Nuxt, SvelteKit, Remix, Gatsby, CRA, Angular, Docusaurus, VitePress, Storybook, Eleventy, Hugo, plain static — plus the runtime kinds (ssr_next, streamlit, gradio, flask, python_web, node_web) and fullstack splits.

The package is extracted from Layero, a deployment platform for frontend applications, where it is the single source of truth shared by the CLI, the control plane, the builder and the backend — one declarative spec and one algorithm instead of four hand-synced re-implementations. It has no dependencies and can be used on its own.


Internals

The single source of truth for how Layero classifies a project (framework, build command, output directory, runtime kind) across every entry point. Replaces the four hand-synced re-implementations (backend framework_detector.py, CLI detect.ts, control-plane dropDeploy/detect.ts, builder frameworks/*.py) and their brittle AST lockstep test (test_framework_sync.py).

What lives here

| File | Role | |---|---| | detection.spec.json | Source of truth. Declarative table: framework identity (signals) + build command + default output + runtime kinds + package managers. Data only — no imperative logic. | | schema.json | JSON Schema for the spec (enforced by gen.mjs). | | detect_core.py | The shared algorithm (Python). detect(Snapshot) → BuildPlan. stdlib-only. | | detect_core.ts | The shared algorithm (TypeScript). In lockstep with the .py for DEFAULT behaviour — enforced by cli/test/detect_core_parity.test.ts in CI. It does NOT carry the opt-in rule set (RULES_V2): those rules are applied by the builder only, on a project's first build, and the builder is authoritative. A CLI that classifies an unusual repo the old way is corrected by the builder on that same first build. | | gen.mjs | Fail-closed generator — vendors the canonical files into each consumer's build context (see below). | | test_parity.py | Golden parity: detect_core vs the real builder as oracle (41 scenarios). | | test_buildplan.py | Shape tests: fullstack, hints, dict-snapshot path, spec↔builder lockstep. |

The model

Snapshot  ──detect()──▶  BuildPlan { project_kind, units[], project_type, … }
(pkg.json,                              │
 files, configs,            BuildUnit { role: static|frontend|backend,
 requirements,                          framework, build_cmd, output_dir,
 layero.json)                           start_cmd, runtime_kind, port }

One result shape natively expresses static / SPA / SSR / runtime / fullstack (a fullstack project is one frontend unit + one backend unit + api_prefix).

Authority: the builder runs detect_core on the cloned disk and is authoritative for identity + output (it alone can discover_served_root over the built tree). The server re-detect is the single writer of project_type. The UI wizard / CLI / drop-deploy all call detect_core too, but advisorily — so they can never persist an identity the builder then rejects (the runtime-config-missing / look-like-mismatch prod failures).

Rules that reclassify ship OFF. A detection change re-decides existing projects on their next deploy, which is why several correct rules were never lowered into this module (next_no_config_is_ssr lived only in the wizard frontend; the fullstack "two folders" rule only in the advisor). New rules are passed in explicitly — detect(snap, hint, RULES_V2) — so one build can compute BOTH verdicts and report the divergence without acting on it (detect_shadow). Turn a rule on only after the shadow report over the real project corpus has been read by a human: python -m app.cli.shadow_detect_corpus replays it over the source archives of every CLI/dashboard project (553 of them) and splits the divergences by "already serving" vs "never shipped". That run caught a defect in the strength rule itself — framework_hint resolved BEFORE signal matching, so the rule would have shipped and done nothing for the exact case it was written for.

Refinement stays in code. Disk-only logic — discover_served_root, the runtime start-command entry-file probe — is NOT in the JSON; it lives in the builder/runtime-builder keyed by the identity this module produces. The spec references such steps by name (output_dir.extract) but never carries logic.

Distribution (why the copies in */_detection/)

core/detection/ is outside every consumer's Docker build context (./backend, ./builder, ./runtime) and outside the CLI's tsconfig rootDir (./src) — a single shared file is mechanically un-importable. So gen.mjs vendors verbatim copies into:

  • backend/app/_detection/from app._detection import detect_core
  • builder/src/_detection/from src._detection import detect_core
  • runtime/builder/app/_detection/from app._detection import detect_core
  • cli/src/_detection/import … from "../_detection/detect_core.js"

A stale copy is a red build: CI runs node core/detection/gen.mjs --check (git diff of the vendored dirs). gen.mjs also refuses a spec whose CONTENT changed without a spec_version bump — until 11.08 that assert compared 1 to 1 forever (the version had never moved since the file was created), so it could not have caught a copy that arrived by any route other than gen.mjs.

Workflow

# edit the canonical files in core/detection/, then:
node core/detection/gen.mjs            # regenerate vendored copies
python3 core/detection/test_parity.py  # parity vs the real builder
python3 core/detection/test_buildplan.py
node core/detection/gen.mjs --check    # what CI asserts

When the builder gains a framework, add a frameworks[] row here and a FrameworkConfig there; test_buildplan.py's lockstep check fails until the names/aliases/default outputs agree.