layero 0.8.12 → 0.8.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,393 @@
1
+ // GENERATED by core/detection/gen.mjs — DO NOT EDIT. Canonical: core/detection/.
2
+ // Unified framework / build-command / output / runtime detection (TypeScript).
3
+ //
4
+ // A faithful port of detect_core.py — SAME spec, SAME algorithm — kept in
5
+ // lockstep with it. PURE: a `Snapshot` in, a `BuildPlan` out, no `node:fs`,
6
+ // so it is safe to vendor into BOTH the CLI (Node) and the control-plane
7
+ // (browser). Each caller builds the Snapshot at its own fidelity (the CLI from
8
+ // the filesystem, drop-deploy from the dropped file tree).
9
+ //
10
+ // stdlib/spec only — vendored verbatim by gen.mjs into <consumer>/_detection/.
11
+ // The spec is emitted as a TS const (spec.ts) so there is no JSON-import-at-
12
+ // runtime friction under tsc + Node ESM. Edit core/detection/, rerun gen.mjs.
13
+ import { SPEC } from "./spec.js";
14
+ const CONFIG_EXTENSIONS = [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"];
15
+ const JS_COMMENT_RE = /\/\/[^\n]*|\/\*[\s\S]*?\*\//gm;
16
+ const NEXT_EXPORT_RE = /output\s*:\s*['"]export['"]/m;
17
+ const NEXT_DISTDIR_RE = /distDir\s*:\s*['"]([^'"]+)['"]/m;
18
+ const OUTPUT_KEY_RE = /\b(?:outDir|distDir|output_dir)\s*[:=]\s*['"]([^'"]+)['"]/;
19
+ const NEXT_CONFIG_NAMES = ["next.config.mjs", "next.config.ts", "next.config.js", "next.config.cjs"];
20
+ function hasDep(pkg, name) {
21
+ if (!pkg)
22
+ return false;
23
+ return Boolean((pkg.dependencies ?? {})[name] ?? (pkg.devDependencies ?? {})[name]);
24
+ }
25
+ function scriptsOf(pkg) {
26
+ return (pkg?.scripts && typeof pkg.scripts === "object") ? pkg.scripts : {};
27
+ }
28
+ export function detectPackageManager(files) {
29
+ const pm = SPEC.package_managers;
30
+ for (const name of pm.order) {
31
+ const mgr = pm.managers[name];
32
+ if (mgr.lockfiles.some((lf) => files.has(lf)))
33
+ return { name, ...mgr, hasLock: true };
34
+ }
35
+ return { name: "npm", ...pm.managers.npm, hasLock: false };
36
+ }
37
+ function configBasenamePresent(snap, basename) {
38
+ return CONFIG_EXTENSIONS.some((ext) => snap.files.has(basename + ext));
39
+ }
40
+ function frameworkMatches(fw, snap) {
41
+ const sig = fw.signals;
42
+ const pkg = snap.packageJson;
43
+ if (sig.always)
44
+ return true;
45
+ for (const dep of sig.deps ?? [])
46
+ if (hasDep(pkg, dep))
47
+ return true;
48
+ for (const cf of sig.config_files ?? [])
49
+ if (snap.files.has(cf))
50
+ return true;
51
+ if (sig.config_basename && configBasenamePresent(snap, sig.config_basename))
52
+ return true;
53
+ for (const name of sig.script_names ?? [])
54
+ if (name in scriptsOf(pkg))
55
+ return true;
56
+ for (const sub of sig.script_contains ?? []) {
57
+ if (Object.values(scriptsOf(pkg)).some((v) => typeof v === "string" && v.includes(sub)))
58
+ return true;
59
+ }
60
+ for (const d of sig.dir_markers ?? [])
61
+ if (snap.dirs.has(d))
62
+ return true;
63
+ if (sig.config_tokens) {
64
+ for (const cf of sig.config_token_files ?? []) {
65
+ if (snap.files.has(cf)) {
66
+ const text = snap.configTexts[cf] ?? "";
67
+ if (sig.config_tokens.some((tok) => text.includes(tok)))
68
+ return true;
69
+ }
70
+ }
71
+ }
72
+ if (sig.html_when_no_spa_deps) {
73
+ if (pkg !== null && (sig.spa_hint_deps ?? []).some((d) => hasDep(pkg, d)))
74
+ return false;
75
+ return snap.hasHtml;
76
+ }
77
+ return false;
78
+ }
79
+ const stripJsComments = (t) => t.replace(JS_COMMENT_RE, "");
80
+ export function nextConfigStaticExport(text) {
81
+ if (text === null || text === undefined)
82
+ return null;
83
+ return NEXT_EXPORT_RE.test(stripJsComments(text));
84
+ }
85
+ function extractOutkey(text) {
86
+ if (!text)
87
+ return null;
88
+ const m = OUTPUT_KEY_RE.exec(text);
89
+ if (!m)
90
+ return null;
91
+ let val = m[1].trim();
92
+ if (val.startsWith("./"))
93
+ val = val.slice(2);
94
+ return val || null;
95
+ }
96
+ function extractNextDistdir(text) {
97
+ if (!text)
98
+ return null;
99
+ const m = NEXT_DISTDIR_RE.exec(stripJsComments(text));
100
+ return m ? m[1] : null;
101
+ }
102
+ export function angularOutputDir(text) {
103
+ if (!text)
104
+ return null;
105
+ let cfg;
106
+ try {
107
+ cfg = JSON.parse(text);
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ if (!cfg || typeof cfg !== "object")
113
+ return null;
114
+ const projects = cfg.projects;
115
+ if (!projects || typeof projects !== "object")
116
+ return null;
117
+ const names = Object.keys(projects);
118
+ if (!names.length)
119
+ return null;
120
+ const def = cfg.defaultProject;
121
+ const name = def && projects[def] ? def : names[0];
122
+ const buildCfg = projects[name]?.architect?.build;
123
+ if (!buildCfg || typeof buildCfg !== "object")
124
+ return null;
125
+ const opts = buildCfg.options ?? {};
126
+ const raw = typeof opts === "object" && opts ? opts.outputPath : undefined;
127
+ let out = typeof raw === "string" && raw.trim() ? raw.trim() : `dist/${name}`;
128
+ if (out.startsWith("./"))
129
+ out = out.slice(2);
130
+ const builder = typeof buildCfg.builder === "string" ? buildCfg.builder : "";
131
+ return builder.endsWith(":application") ? `${out}/browser` : out;
132
+ }
133
+ function nextConfigName(snap) {
134
+ for (const n of NEXT_CONFIG_NAMES)
135
+ if (n in snap.configTexts || snap.files.has(n))
136
+ return n;
137
+ return NEXT_CONFIG_NAMES[0];
138
+ }
139
+ function configTextForBasename(snap, basename) {
140
+ for (const ext of CONFIG_EXTENSIONS) {
141
+ const t = snap.configTexts[basename + ext];
142
+ if (t !== undefined)
143
+ return t;
144
+ }
145
+ return null;
146
+ }
147
+ function resolveOutputDir(fw, snap) {
148
+ const out = fw.output_dir;
149
+ const def = out.default;
150
+ switch (out.extract) {
151
+ case "next_distdir":
152
+ return extractNextDistdir(snap.configTexts[nextConfigName(snap)]) ?? def;
153
+ case "outkey":
154
+ return extractOutkey(configTextForBasename(snap, out.extract_config_basename ?? "")) ?? def;
155
+ case "angular":
156
+ return angularOutputDir(snap.configTexts["angular.json"]) ?? def;
157
+ case "vitepress_docs":
158
+ return [...snap.files].some((f) => f.startsWith("docs/.vitepress/config")) ? "docs/.vitepress/dist" : def;
159
+ default:
160
+ return def;
161
+ }
162
+ }
163
+ function resolveBuildCmd(fw, snap, pmRun) {
164
+ const build = fw.build;
165
+ if (build.no_build)
166
+ return null;
167
+ const scripts = scriptsOf(snap.packageJson);
168
+ for (const entry of build.scripts ?? []) {
169
+ const val = scripts[entry.name];
170
+ if (typeof val !== "string" || !val.trim())
171
+ continue;
172
+ if (entry.contains && !val.includes(entry.contains))
173
+ continue;
174
+ return `${pmRun} ${entry.name}`;
175
+ }
176
+ const fallback = build.fallback;
177
+ if (!fallback)
178
+ return null;
179
+ return build.npx === false ? fallback : `npx ${fallback}`;
180
+ }
181
+ function sortedFrameworks() {
182
+ return [...SPEC.frameworks].sort((a, b) => a.order - b.order);
183
+ }
184
+ function matchFramework(snap) {
185
+ for (const fw of sortedFrameworks())
186
+ if (frameworkMatches(fw, snap))
187
+ return fw;
188
+ return SPEC.frameworks.find((f) => f.name === "generic");
189
+ }
190
+ function frameworkByName(name) {
191
+ const n = (name ?? "").trim().toLowerCase();
192
+ if (!n)
193
+ return null;
194
+ for (const fw of SPEC.frameworks)
195
+ if (fw.name === n || (fw.aliases ?? []).includes(n))
196
+ return fw;
197
+ return null;
198
+ }
199
+ function staticUnit(pm) {
200
+ return {
201
+ role: "static", framework: "static", root: "", packageManager: pm.name,
202
+ installCmd: null, buildCmd: null, outputDir: ".", startCmd: null, runtimeKind: null, port: null,
203
+ };
204
+ }
205
+ function spaUnit(fw, snap, pm) {
206
+ return {
207
+ role: "static", framework: fw.name, root: "", packageManager: pm.name,
208
+ installCmd: fw.skip_install ? null : pm.install,
209
+ buildCmd: resolveBuildCmd(fw, snap, pm.run),
210
+ outputDir: resolveOutputDir(fw, snap),
211
+ startCmd: null, runtimeKind: null, port: null,
212
+ };
213
+ }
214
+ export function detectFramework(snap, frameworkHint) {
215
+ const pm = detectPackageManager(snap.files);
216
+ let unknown = null;
217
+ let fw = null;
218
+ if (frameworkHint) {
219
+ fw = frameworkByName(frameworkHint);
220
+ if (fw === null)
221
+ unknown = frameworkHint;
222
+ }
223
+ if (fw === null)
224
+ fw = matchFramework(snap);
225
+ if (fw.name === "static")
226
+ return [staticUnit(pm), unknown];
227
+ return [spaUnit(fw, snap, pm), unknown];
228
+ }
229
+ export function detectRuntime(snap) {
230
+ const rt = SPEC.runtime;
231
+ const pkg = snap.packageJson;
232
+ if (hasDep(pkg, "next")) {
233
+ const text = snap.configTexts[nextConfigName(snap)];
234
+ if (nextConfigStaticExport(text ?? null) === false)
235
+ return "ssr_next";
236
+ }
237
+ if (pkg && rt.node_web_signals.some((d) => hasDep(pkg, d)) &&
238
+ !rt.node_frontend_deps.some((d) => hasDep(pkg, d))) {
239
+ return "node_web";
240
+ }
241
+ const reqs = (snap.requirementsTxt ?? "").toLowerCase();
242
+ if (reqs) {
243
+ const hasEntry = rt.python_entrypoints.some((f) => snap.files.has(f));
244
+ if (reqs.includes("streamlit") && snap.files.has("app.py"))
245
+ return "streamlit";
246
+ if (reqs.includes("gradio") && snap.files.has("app.py"))
247
+ return "gradio";
248
+ if (hasEntry && rt.py_web_signals.some((s) => reqs.includes(s)))
249
+ return "python_web";
250
+ }
251
+ return null;
252
+ }
253
+ function runtimeFromLayero(layero) {
254
+ if (!layero || typeof layero !== "object")
255
+ return null;
256
+ const rt = SPEC.runtime;
257
+ const aliases = rt.layero_runtime_aliases;
258
+ let kind = layero.runtime;
259
+ if (typeof kind === "string") {
260
+ kind = aliases[kind] ?? kind;
261
+ if (kind in rt.kinds)
262
+ return [kind, layero];
263
+ }
264
+ const fe = layero.frontend, be = layero.backend;
265
+ if (fe && typeof fe === "object" && be && typeof be === "object") {
266
+ let beKind = be.runtime;
267
+ if (typeof beKind === "string") {
268
+ beKind = aliases[beKind] ?? beKind;
269
+ if (["python_web", "node_web", "ssr_next"].includes(beKind))
270
+ return [`__fullstack__:${beKind}`, layero];
271
+ }
272
+ }
273
+ return null;
274
+ }
275
+ const PY_FRAMEWORK_ORDER = [
276
+ ["django", "django"], ["fastapi", "fastapi"], ["flask", "flask"], ["litestar", "litestar"],
277
+ ["starlite", "litestar"], ["quart", "quart"], ["sanic", "sanic"], ["blacksheep", "blacksheep"],
278
+ ["falcon", "falcon"], ["bottle", "bottle"], ["pyramid", "pyramid"], ["aiohttp", "aiohttp"],
279
+ ["tornado", "tornado"], ["starlette", "starlette"],
280
+ ];
281
+ const NODE_FRAMEWORK_ORDER = [
282
+ ["@nestjs/core", "nestjs"], ["@adonisjs/core", "adonis"], ["fastify", "fastify"],
283
+ ["@hapi/hapi", "hapi"], ["hapi", "hapi"], ["koa", "koa"], ["hono", "hono"],
284
+ ["restify", "restify"], ["express", "express"],
285
+ ];
286
+ export function detectRuntimeFramework(snap) {
287
+ const reqs = (snap.requirementsTxt ?? "").toLowerCase();
288
+ if (reqs)
289
+ for (const [token, name] of PY_FRAMEWORK_ORDER)
290
+ if (reqs.includes(token))
291
+ return name;
292
+ if (snap.packageJson)
293
+ for (const [dep, name] of NODE_FRAMEWORK_ORDER)
294
+ if (hasDep(snap.packageJson, dep))
295
+ return name;
296
+ return null;
297
+ }
298
+ function runtimePlan(snap, pm, projectType) {
299
+ const meta = SPEC.runtime.kinds[projectType];
300
+ const framework = detectRuntimeFramework(snap) ?? projectType;
301
+ const unit = {
302
+ role: "backend", framework, root: "",
303
+ packageManager: snap.packageJson ? pm.name : null,
304
+ installCmd: null, buildCmd: null, outputDir: null,
305
+ startCmd: null, runtimeKind: meta.runtime_kind, port: meta.port,
306
+ };
307
+ return {
308
+ projectKind: projectType === "ssr_next" ? "ssr" : "runtime",
309
+ framework, buildCmd: null, outputDir: null, packageManager: pm.name,
310
+ projectType, runtimeKind: meta.runtime_kind, units: [unit],
311
+ apiPrefix: null, confident: true, label: projectType,
312
+ };
313
+ }
314
+ function fullstackPlan(_snap, pm, layero, beProjectType) {
315
+ const meta = SPEC.runtime.kinds[beProjectType];
316
+ const fe = (layero.frontend ?? {});
317
+ const be = (layero.backend ?? {});
318
+ const apiPrefix = String(layero.api_prefix ?? "/api").trim() || "/api";
319
+ const feUnit = {
320
+ role: "frontend", framework: String(fe.framework ?? "generic"),
321
+ root: String(fe.root ?? "").replace(/^\/+|\/+$/g, ""),
322
+ packageManager: null, installCmd: null,
323
+ buildCmd: fe.build ? String(fe.build) : null,
324
+ outputDir: String(fe.output ?? "dist").replace(/^\/+|\/+$/g, ""),
325
+ startCmd: null, runtimeKind: null, port: null,
326
+ };
327
+ const beUnit = {
328
+ role: "backend", framework: beProjectType,
329
+ root: String(be.root ?? "").replace(/^\/+|\/+$/g, ""),
330
+ packageManager: null, installCmd: null, buildCmd: null, outputDir: null,
331
+ startCmd: be.start ? String(be.start) : null,
332
+ runtimeKind: meta.runtime_kind, port: Number(be.port ?? meta.port),
333
+ };
334
+ return {
335
+ projectKind: "fullstack", framework: beProjectType, buildCmd: null, outputDir: null,
336
+ packageManager: pm.name, projectType: beProjectType, runtimeKind: meta.runtime_kind,
337
+ units: [feUnit, beUnit], apiPrefix, confident: true, label: "Fullstack",
338
+ };
339
+ }
340
+ export function detect(snap, frameworkHint) {
341
+ const pm = detectPackageManager(snap.files);
342
+ const lj = runtimeFromLayero(snap.layeroJson);
343
+ if (lj !== null) {
344
+ const [kind, layero] = lj;
345
+ if (kind.startsWith("__fullstack__:"))
346
+ return fullstackPlan(snap, pm, layero, kind.split(":")[1]);
347
+ return runtimePlan(snap, pm, kind);
348
+ }
349
+ const rkind = detectRuntime(snap);
350
+ if (rkind !== null)
351
+ return runtimePlan(snap, pm, rkind);
352
+ let fw = frameworkHint ? frameworkByName(frameworkHint) : null;
353
+ if (fw === null)
354
+ fw = matchFramework(snap);
355
+ let unit;
356
+ let kind;
357
+ let confident;
358
+ if (fw.name === "static") {
359
+ unit = staticUnit(pm);
360
+ kind = "static";
361
+ confident = snap.hasHtml;
362
+ }
363
+ else {
364
+ unit = spaUnit(fw, snap, pm);
365
+ kind = "spa";
366
+ confident = fw.name !== "generic";
367
+ }
368
+ return {
369
+ projectKind: kind, framework: fw.name, buildCmd: unit.buildCmd, outputDir: unit.outputDir,
370
+ packageManager: pm.name, projectType: "spa", runtimeKind: null, units: [unit],
371
+ apiPrefix: null, confident, label: fw.label,
372
+ };
373
+ }
374
+ /** Build a Snapshot from already-read inputs (caller does the IO). */
375
+ export function snapshotFromInputs(opts) {
376
+ const files = new Set(opts.files ?? []);
377
+ const hasHtml = opts.hasHtml ?? [...files].some((f) => f.endsWith(".html") || f.endsWith(".htm"));
378
+ return {
379
+ packageJson: opts.packageJson ?? null,
380
+ requirementsTxt: opts.requirementsTxt ?? null,
381
+ files,
382
+ dirs: new Set(opts.dirs ?? []),
383
+ configTexts: opts.configTexts ?? {},
384
+ hasHtml,
385
+ layeroJson: opts.layeroJson ?? null,
386
+ };
387
+ }
388
+ // Config files whose CONTENT the detector reads (for the caller's snapshot builder).
389
+ export const CONFIG_TEXT_FILES = [
390
+ ...NEXT_CONFIG_NAMES, "angular.json", "config.toml", "config.yaml", "config.json",
391
+ ];
392
+ export const CONFIG_TEXT_BASENAMES = ["vite.config", "astro.config"];
393
+ export { CONFIG_EXTENSIONS };