// Reine Projekterkennung (Spec 1.2 „Erkennung“): static | build | dev, Paketmanager, Befehle. import { statSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; const DEFAULT_OUTPUTS = ['dist', 'build', 'out', 'web-build']; const DEFAULT_PORT = 5173; const safeRel = (p) => typeof p === 'string' && p !== '' && p.length <= 200 && !p.startsWith('/') && !p.includes('\0') && !p.split(/[\\/]/).includes('..'); function scriptsOf(files) { try { const txt = files.read('package.json'); if (txt == null) return {}; const s = JSON.parse(txt)?.scripts; return s && typeof s === 'object' && !Array.isArray(s) ? s : {}; } catch { return {}; } } const hasScript = (scripts, name) => typeof scripts[name] === 'string' && scripts[name].trim() !== ''; function detectPm(files) { if (files.has('pnpm-lock.yaml')) return 'pnpm'; if (files.has('yarn.lock')) return 'yarn'; if (files.has('bun.lockb') || files.has('bun.lock')) return 'bun'; return 'npm'; } function installCmd(pm, files) { if (pm === 'npm') { return files.has('package-lock.json') || files.has('npm-shrinkwrap.json') ? ['npm', 'ci', '--include=dev'] : ['npm', 'install', '--include=dev', '--no-audit', '--no-fund']; } return [pm, 'install', '--frozen-lockfile']; } // npm/pnpm brauchen `--` vor den Skript-Argumenten, yarn/bun reichen sie direkt durch. function devCmdFor(pm, port) { const args = ['--host', '0.0.0.0', '--port', String(port)]; return pm === 'npm' || pm === 'pnpm' ? [pm, 'run', 'dev', '--', ...args] : [pm, 'run', 'dev', ...args]; } export function detectProject(files, { mode, index, previewJson } = {}) { const pj = previewJson && typeof previewJson === 'object' && !Array.isArray(previewJson) ? previewJson : {}; const scripts = scriptsOf(files); const pm = detectPm(files); const devPort = Number.isInteger(pj.devPort) && pj.devPort >= 1024 && pj.devPort <= 65535 ? pj.devPort : DEFAULT_PORT; const outputDirs = safeRel(pj.output) ? [pj.output] : [...DEFAULT_OUTPUTS]; const indexFile = [index, pj.index].find(safeRel) || 'index.html'; const hasIndex = files.has(indexFile); const hasBuild = hasScript(scripts, 'build'); const hasDev = hasScript(scripts, 'dev'); const m = mode === 'build' || mode === 'dev' ? mode : 'auto'; const base = { pm, install: installCmd(pm, files), buildCmd: hasBuild ? [pm, 'run', 'build'] : null, devCmd: hasDev ? devCmdFor(pm, devPort) : null, outputDirs, devPort }; const asBuild = () => ({ kind: 'build', ...base }); const asDev = () => ({ kind: 'dev', ...base }); const asStatic = (hint) => ({ kind: 'static', ...base, install: [], buildCmd: null, devCmd: null, outputDirs: [], index: indexFile, ...(hint ? { hint } : {}) }); if (m === 'build') return hasBuild ? asBuild() : { ...asBuild(), error: 'Kein build-Skript in package.json' }; if (m === 'dev') return hasDev ? asDev() : { ...asDev(), error: 'Kein dev-Skript in package.json' }; if (hasIndex && !hasBuild) return asStatic(); if (hasBuild) return asBuild(); if (hasDev) return asDev(); return asStatic(`Hinweis: Weder ${indexFile} noch ein build-/dev-Skript gefunden – Inhalt wird unverändert als statische Seite bereitgestellt.`); } // Dateisystem-Adapter für detectProject; previewJson wird aus .webklar-preview.json gelesen, wenn nicht übergeben. export function detectFromDir(dir, opts = {}) { const abs = (p) => join(dir, p); const files = { has: (p) => { try { return statSync(abs(p)).isFile(); } catch { return false; } }, read: (p) => { try { return readFileSync(abs(p), 'utf8'); } catch { return null; } }, }; let previewJson = opts.previewJson; if (previewJson === undefined) { try { previewJson = JSON.parse(files.read('.webklar-preview.json')); } catch { previewJson = {}; } } return detectProject(files, { ...opts, previewJson }); }