layero 0.7.1 → 0.7.3
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.
- package/dist/commands/deploy.js +1 -0
- package/dist/detect.js +149 -1
- package/package.json +4 -2
package/dist/commands/deploy.js
CHANGED
|
@@ -214,6 +214,7 @@ async function resolveSetupConfig(cwd, opts, existing) {
|
|
|
214
214
|
output_dir: detected.output_dir,
|
|
215
215
|
confident: detected.confident,
|
|
216
216
|
...(detected.runtime_kind ? { runtime_kind: detected.runtime_kind } : {}),
|
|
217
|
+
...(detected.ssr_warning ? { ssr_warning: detected.ssr_warning } : {}),
|
|
217
218
|
});
|
|
218
219
|
const framework_hint = opts.type ?? existing?.framework_hint ?? detected.framework_hint;
|
|
219
220
|
const build_cmd = existing?.build_cmd ?? detected.build_cmd;
|
package/dist/detect.js
CHANGED
|
@@ -32,6 +32,13 @@ async function fileExists(cwd, ...candidates) {
|
|
|
32
32
|
// hit on 2026-05-22 (builder detector v72 fix) and 2026-05-26 (CLI side)
|
|
33
33
|
// was exactly this kind of dual detector drift.
|
|
34
34
|
const NEXT_STATIC_EXPORT_RE = /output\s*:\s*['"]export['"]/m;
|
|
35
|
+
// Strip JS line and block comments before regex matching. Without this,
|
|
36
|
+
// a comment like `// see output: 'export'` falsely matches as
|
|
37
|
+
// static-export — observed 2026-05-26 on the SSR smoke canary fixture.
|
|
38
|
+
// Naive (does not understand strings that contain comment-like tokens),
|
|
39
|
+
// but real next.config files don't have that shape.
|
|
40
|
+
const JS_COMMENT_RE = /\/\/[^\n]*|\/\*[\s\S]*?\*\//gm;
|
|
41
|
+
const stripJsComments = (text) => text.replace(JS_COMMENT_RE, "");
|
|
35
42
|
async function readNextConfigText(cwd) {
|
|
36
43
|
for (const name of ["next.config.mjs", "next.config.ts", "next.config.js", "next.config.cjs"]) {
|
|
37
44
|
try {
|
|
@@ -52,7 +59,7 @@ async function nextConfigStaticExport(cwd) {
|
|
|
52
59
|
const text = await readNextConfigText(cwd);
|
|
53
60
|
if (text === null)
|
|
54
61
|
return null;
|
|
55
|
-
return NEXT_STATIC_EXPORT_RE.test(text);
|
|
62
|
+
return NEXT_STATIC_EXPORT_RE.test(stripJsComments(text));
|
|
56
63
|
}
|
|
57
64
|
async function hasAnyHtml(cwd) {
|
|
58
65
|
// Shallow check — only top-level. Recursive walk is too expensive here
|
|
@@ -112,19 +119,69 @@ export async function detectProject(cwd) {
|
|
|
112
119
|
: buildScript
|
|
113
120
|
? "npm run build"
|
|
114
121
|
: "npx nuxt generate";
|
|
122
|
+
// Nuxt defaults to SSR. Layero hosts only static for Nuxt today,
|
|
123
|
+
// so a project without an explicit static signal (`nuxt generate`
|
|
124
|
+
// script, or `ssr: false` / `nitro.preset='static'` in config)
|
|
125
|
+
// will build but the resulting `.output/server/` is useless to us.
|
|
126
|
+
// Mirrors builder's `frameworks/nuxt.py` SSR hint.
|
|
127
|
+
const nuxtStatic = !!generateScript || (await nuxtConfigDeclaresStatic(cwd));
|
|
115
128
|
return {
|
|
116
129
|
framework_hint: "nuxt",
|
|
117
130
|
build_cmd: cmd,
|
|
118
131
|
output_dir: ".output/public",
|
|
119
132
|
confident: true,
|
|
133
|
+
...(nuxtStatic ? {} : {
|
|
134
|
+
ssr_warning: "Nuxt без `nuxt generate` или `ssr: false` собирается как SSR-сервер; " +
|
|
135
|
+
"Layero хостит только статику Nuxt. Добавьте `\"generate\": \"nuxt generate\"` " +
|
|
136
|
+
"в scripts или поставьте `ssr: false` в nuxt.config.",
|
|
137
|
+
}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// Remix / React Router v7 before SvelteKit/Vite — RR7 ships Vite
|
|
141
|
+
// internally, so the Vite-dep check would otherwise win. Mirrors the
|
|
142
|
+
// builder's RemixFramework + backend `remix` table entry (output
|
|
143
|
+
// build/client). Was missing from the CLI entirely — same dual-detector
|
|
144
|
+
// gap class as Angular: a Remix repo deployed via the CLI fell through
|
|
145
|
+
// to the static fallback and shipped raw sources.
|
|
146
|
+
if (hasDep(pkg, "@remix-run/dev") ||
|
|
147
|
+
hasDep(pkg, "@remix-run/react") ||
|
|
148
|
+
hasDep(pkg, "@remix-run/node") ||
|
|
149
|
+
hasDep(pkg, "@react-router/dev") ||
|
|
150
|
+
hasDep(pkg, "@react-router/node") ||
|
|
151
|
+
(await fileExists(cwd, "react-router.config.ts", "react-router.config.js"))) {
|
|
152
|
+
return {
|
|
153
|
+
framework_hint: "remix",
|
|
154
|
+
build_cmd: buildCmd(pkg, "npx react-router build"),
|
|
155
|
+
output_dir: "build/client",
|
|
156
|
+
confident: true,
|
|
120
157
|
};
|
|
121
158
|
}
|
|
122
159
|
if (hasDep(pkg, "@sveltejs/kit") || (await fileExists(cwd, "svelte.config.js"))) {
|
|
160
|
+
// SvelteKit needs an adapter. adapter-static = SPA path; anything
|
|
161
|
+
// else (adapter-node, adapter-auto, …) is server-side. Mirrors
|
|
162
|
+
// builder's `frameworks/svelte.py:validate_static`.
|
|
163
|
+
const hasStaticAdapter = hasDep(pkg, "@sveltejs/adapter-static");
|
|
164
|
+
const serverAdapter = !hasStaticAdapter
|
|
165
|
+
? Object.keys({ ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) })
|
|
166
|
+
.find((d) => d.startsWith("@sveltejs/adapter-") && d !== "@sveltejs/adapter-static")
|
|
167
|
+
: undefined;
|
|
168
|
+
let ssrWarning;
|
|
169
|
+
if (serverAdapter) {
|
|
170
|
+
ssrWarning =
|
|
171
|
+
`SvelteKit использует ${serverAdapter} (серверный адаптер). ` +
|
|
172
|
+
"Layero хостит только статику — поставьте `@sveltejs/adapter-static`.";
|
|
173
|
+
}
|
|
174
|
+
else if (!hasStaticAdapter) {
|
|
175
|
+
ssrWarning =
|
|
176
|
+
"SvelteKit без явного адаптера — поставьте `@sveltejs/adapter-static` " +
|
|
177
|
+
"для статического хостинга на Layero.";
|
|
178
|
+
}
|
|
123
179
|
return {
|
|
124
180
|
framework_hint: "sveltekit",
|
|
125
181
|
build_cmd: buildCmd(pkg, "npx vite build"),
|
|
126
182
|
output_dir: "build",
|
|
127
183
|
confident: true,
|
|
184
|
+
...(ssrWarning ? { ssr_warning: ssrWarning } : {}),
|
|
128
185
|
};
|
|
129
186
|
}
|
|
130
187
|
if (hasDep(pkg, "gatsby") || (await fileExists(cwd, "gatsby-config.js", "gatsby-config.ts"))) {
|
|
@@ -216,6 +273,22 @@ export async function detectProject(cwd) {
|
|
|
216
273
|
confident: true,
|
|
217
274
|
};
|
|
218
275
|
}
|
|
276
|
+
// Angular after Vite — mirrors builder ALL order. Angular ships its
|
|
277
|
+
// own CLI (`ng build`) and writes to `dist/{project}/` (Angular <17)
|
|
278
|
+
// or `dist/{project}/browser/` (17+ `application` builder), NOT bare
|
|
279
|
+
// `dist`. Until this branch existed the CLI fell through to the
|
|
280
|
+
// static fallback (`output_dir='.'`), so the first deploy of an
|
|
281
|
+
// Angular repo shipped the raw sources — the dist/<project> S3 404
|
|
282
|
+
// incident. `angularOutputDir` parses angular.json to pin the path
|
|
283
|
+
// (lockstep with `frameworks/angular.py:extract_output_dir`).
|
|
284
|
+
if (hasDep(pkg, "@angular/core") || (await fileExists(cwd, "angular.json"))) {
|
|
285
|
+
return {
|
|
286
|
+
framework_hint: "angular",
|
|
287
|
+
build_cmd: buildCmd(pkg, "npx ng build"),
|
|
288
|
+
output_dir: await angularOutputDir(cwd),
|
|
289
|
+
confident: true,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
219
292
|
if (hasDep(pkg, "react-scripts")) {
|
|
220
293
|
return {
|
|
221
294
|
framework_hint: "cra",
|
|
@@ -279,6 +352,27 @@ const HUGO_CONFIG_TOKENS = [
|
|
|
279
352
|
"minVersion",
|
|
280
353
|
"theme",
|
|
281
354
|
];
|
|
355
|
+
// Match `ssr: false` or `nitro: { preset: 'static' }` in nuxt.config —
|
|
356
|
+
// either marker keeps the project on the SPA pipeline. Mirrors the same
|
|
357
|
+
// "string-search before AST" approach used for Next.js.
|
|
358
|
+
const NUXT_STATIC_SSR_RE = /\bssr\s*:\s*false\b/m;
|
|
359
|
+
const NUXT_STATIC_PRESET_RE = /preset\s*:\s*['"]static['"]/m;
|
|
360
|
+
async function nuxtConfigDeclaresStatic(cwd) {
|
|
361
|
+
for (const name of ["nuxt.config.mjs", "nuxt.config.ts", "nuxt.config.js"]) {
|
|
362
|
+
try {
|
|
363
|
+
const txt = stripJsComments(await fs.readFile(path.join(cwd, name), "utf-8"));
|
|
364
|
+
if (NUXT_STATIC_SSR_RE.test(txt) || NUXT_STATIC_PRESET_RE.test(txt))
|
|
365
|
+
return true;
|
|
366
|
+
return false; // config found, no static marker → SSR
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
if (err?.code === "ENOENT")
|
|
370
|
+
continue;
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return false; // no config at all → defaults to SSR
|
|
375
|
+
}
|
|
282
376
|
async function hasVitepressConfig(cwd, prefix = ".vitepress") {
|
|
283
377
|
for (const name of ["config.ts", "config.js", "config.mts", "config.mjs"]) {
|
|
284
378
|
try {
|
|
@@ -304,3 +398,57 @@ async function hasHugoConfigMarker(cwd) {
|
|
|
304
398
|
}
|
|
305
399
|
return false;
|
|
306
400
|
}
|
|
401
|
+
// Resolve Angular's real build output directory from angular.json.
|
|
402
|
+
// Lockstep with `core/builder/src/frameworks/angular.py:extract_output_dir`
|
|
403
|
+
// — same dual-detector-drift class as Next.js static-export, so the two
|
|
404
|
+
// implementations must agree (parity fixtures in
|
|
405
|
+
// core/tests/fixtures/framework-detect/angular-*).
|
|
406
|
+
//
|
|
407
|
+
// Resolution:
|
|
408
|
+
// 1. defaultProject if set & present, else first project in `projects`
|
|
409
|
+
// 2. project.architect.build.options.outputPath, else the CLI default
|
|
410
|
+
// `dist/{projectName}`
|
|
411
|
+
// 3. strip leading `./`
|
|
412
|
+
// 4. append `/browser` for the Angular 17+ `application` builder
|
|
413
|
+
// (builder id ends with `:application`) — it writes the served
|
|
414
|
+
// assets one level deeper. We also probe disk in case the repo was
|
|
415
|
+
// built locally before `layero deploy`.
|
|
416
|
+
// Returns the framework default `dist` on any parse error — matches
|
|
417
|
+
// AngularFramework.default_output_dir, and the builder re-derives the
|
|
418
|
+
// exact path at upload time anyway.
|
|
419
|
+
async function angularOutputDir(cwd) {
|
|
420
|
+
const FALLBACK = "dist";
|
|
421
|
+
let cfg;
|
|
422
|
+
try {
|
|
423
|
+
cfg = JSON.parse(await fs.readFile(path.join(cwd, "angular.json"), "utf-8"));
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return FALLBACK;
|
|
427
|
+
}
|
|
428
|
+
const projects = cfg?.projects;
|
|
429
|
+
if (!projects || typeof projects !== "object")
|
|
430
|
+
return FALLBACK;
|
|
431
|
+
const firstName = Object.keys(projects)[0];
|
|
432
|
+
if (!firstName)
|
|
433
|
+
return FALLBACK;
|
|
434
|
+
const defaultName = cfg.defaultProject;
|
|
435
|
+
const projectName = defaultName && projects[defaultName] ? defaultName : firstName;
|
|
436
|
+
const buildCfg = projects[projectName]?.architect?.build;
|
|
437
|
+
if (!buildCfg || typeof buildCfg !== "object")
|
|
438
|
+
return FALLBACK;
|
|
439
|
+
const opts = (buildCfg.options ?? {});
|
|
440
|
+
const rawOut = typeof opts === "object" && opts ? opts.outputPath : undefined;
|
|
441
|
+
let out = typeof rawOut === "string" && rawOut.trim() ? rawOut.trim() : `dist/${projectName}`;
|
|
442
|
+
if (out.startsWith("./"))
|
|
443
|
+
out = out.slice(2);
|
|
444
|
+
const builderId = typeof buildCfg.builder === "string" ? buildCfg.builder : "";
|
|
445
|
+
const isApplicationBuilder = builderId.endsWith(":application");
|
|
446
|
+
let browserExists = false;
|
|
447
|
+
try {
|
|
448
|
+
browserExists = (await fs.stat(path.join(cwd, out, "browser"))).isDirectory();
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
// not built locally — rely on the builder-id signal below
|
|
452
|
+
}
|
|
453
|
+
return browserExists || isApplicationBuilder ? `${out}/browser` : out;
|
|
454
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "layero",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Layero CLI — publish a local site with one command. No git, no GitHub, agent-friendly (Cursor, Claude Code).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsc -p tsconfig.json",
|
|
43
43
|
"prepublishOnly": "npm run build",
|
|
44
|
+
"test": "vitest run",
|
|
44
45
|
"dev": "tsc -p tsconfig.json --watch",
|
|
45
46
|
"postinstall": "node scripts/postinstall.cjs || true"
|
|
46
47
|
},
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@types/node": "^20.14.10",
|
|
56
|
-
"typescript": "^5.5.4"
|
|
57
|
+
"typescript": "^5.5.4",
|
|
58
|
+
"vitest": "^4.1.7"
|
|
57
59
|
}
|
|
58
60
|
}
|