layero-detection
| Install | |
|---|---|
npm install layero-detection |
|
| Latest Version: | 0.1.12 |
| License: | MIT |
| Last Updated: | Sep 29, 2026 |
| Links: | npm |
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.
- Platform: https://layero.ru · Docs: https://docs.layero.ru
- The CLI that consumes it:
layero
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.
Кто вправе применять эти правила — решает ядро, а не билдер.
rules_for_build(ctx) отвечает «первая сборка, тип не выбран человеком, архив
не готовый артефакт» и живёт здесь (26.08.2026). Раньше условие стояло в
builder/src/pipeline.py, и это работало, пока применял правила один билдер.
С single_app_fullstack их применяет и рантайм-билдер — другая деплой-единица,
— а вторая копия условия означала бы, что «только новые проекты» у них
разъедется. Оба ИСПОЛНЯЮТ функцию, гейт cli/check-detection-authority.py
сверяет её ТАБЛИЦУ ИСТИННОСТИ, а не текст (текстовая редакция оказалась
вакуумной: условие вынимается из выражения, строка остаётся в файле рядом).
Правило может быть выкачено наполовину, и это тише отказа. Вердикт —
только половина пути: раскладку ещё должна получить СБОРКА. У
single_app_fullstack fullstack_config не пишет никто (его пишет только
панель), поэтому рантайм-билдер выводит раскладку сам, тем же правилом и под
тем же гейтом. Без этого шага детект называл бы проект фуллстеком, а сборка
собирала обычный контейнер — зелено, без фронта и без единого слова.
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_corebuilder/src/_detection/→from src._detection import detect_coreruntime/builder/app/_detection/→from app._detection import detect_corecli/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.
Related Packages
Framework-agnostic runtime cache invalidation for Strapi v5 in SSR frontends. Re...
Retake: a time machine for your dev server (Vite, Next.js, React Router, Remix,...
Version History
| Version | Released | License |
|---|---|---|
| 0.1.12 | MIT | |
| 0.1.11 | MIT | |
| 0.1.10 | MIT | |
| 0.1.9 | MIT | |
| 0.1.8 | MIT | |
| 0.1.7 | MIT | |
| 0.1.6 | MIT | |
| 0.1.5 | MIT | |
| 0.1.4 | MIT | |
| 0.1.3 | MIT | |
| 0.1.2 | MIT | |
| 0.1.1 | MIT | |
| 0.1.0 | MIT |