layero-detection

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.
5,271
Install
npm install layero-detection
Latest Version:0.1.12
License:MIT
Last Updated:Sep 29, 2026
Links: npm
Maintainer: napitok

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.

Кто вправе применять эти правила — решает ядро, а не билдер. 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_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.

Related Packages

@datum-cloud/strapi-revalidate

Framework-agnostic runtime cache invalidation for Strapi v5 in SSR frontends. Re...

2,396 0
eurobase

The official Eurobase CLI for deployment debugging and deployments

1,569
retake-dev

Retake: a time machine for your dev server (Vite, Next.js, React Router, Remix,...

1,295 2
type-envgen

CLI that generates a typesafe env file validated with zod

811 3
sitegate

A secure, self-hosted password gate for staging and preview websites.

1,488 0

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