import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { detectProject, detectFromDir } from '../src/detect.js'; const fake = (map) => ({ has: (p) => Object.hasOwn(map, p), read: (p) => (Object.hasOwn(map, p) ? map[p] : null), }); const pkg = (scripts) => JSON.stringify({ scripts }); test('HTML ohne package.json ⇒ static', () => { const r = detectProject(fake({ 'index.html': '' }), {}); assert.equal(r.kind, 'static'); assert.equal(r.buildCmd, null); assert.equal(r.devCmd, null); }); test('Vite mit build ⇒ build, npm', () => { const r = detectProject(fake({ 'package.json': pkg({ build: 'vite build', dev: 'vite' }) }), { mode: 'auto' }); assert.equal(r.kind, 'build'); assert.equal(r.pm, 'npm'); assert.deepEqual(r.install, ['npm', 'install', '--include=dev', '--no-audit', '--no-fund']); assert.deepEqual(r.buildCmd, ['npm', 'run', 'build']); assert.deepEqual(r.outputDirs, ['dist', 'build', 'out', 'web-build']); assert.equal(r.devPort, 5173); }); test('npm mit Lock ⇒ npm ci', () => { const r = detectProject(fake({ 'package.json': pkg({ build: 'x' }), 'package-lock.json': '{}' }), {}); assert.deepEqual(r.install, ['npm', 'ci', '--include=dev']); }); test('pnpm-lock ⇒ pnpm', () => { const r = detectProject(fake({ 'package.json': pkg({ build: 'x', dev: 'y' }), 'pnpm-lock.yaml': '' }), {}); assert.equal(r.pm, 'pnpm'); assert.deepEqual(r.install, ['pnpm', 'install', '--frozen-lockfile']); assert.deepEqual(r.buildCmd, ['pnpm', 'run', 'build']); assert.deepEqual(r.devCmd, ['pnpm', 'run', 'dev', '--host', '0.0.0.0', '--port', '5173']); }); test('yarn Berry (.yarnrc.yml) ⇒ --immutable', () => { const r = detectProject(fake({ 'package.json': pkg({ build: 'x' }), 'yarn.lock': '', '.yarnrc.yml': '' }), {}); assert.deepEqual(r.install, ['yarn', 'install', '--immutable']); }); test('yarn.lock und bun.lockb/bun.lock', () => { assert.deepEqual(detectProject(fake({ 'package.json': pkg({ build: 'x' }), 'yarn.lock': '' }), {}).install, ['yarn', 'install', '--frozen-lockfile']); for (const lock of ['bun.lockb', 'bun.lock']) { const r = detectProject(fake({ 'package.json': pkg({ build: 'x' }), [lock]: '' }), {}); assert.equal(r.pm, 'bun'); assert.deepEqual(r.install, ['bun', 'install', '--frozen-lockfile']); assert.deepEqual(r.buildCmd, ['bun', 'run', 'build']); } }); test('nur dev-Skript ⇒ dev', () => { const r = detectProject(fake({ 'package.json': pkg({ dev: 'vite' }) }), {}); assert.equal(r.kind, 'dev'); assert.equal(r.buildCmd, null); assert.deepEqual(r.devCmd, ['npm', 'run', 'dev', '--', '--host', '0.0.0.0', '--port', '5173']); }); test('index.html + build-Skript ⇒ build', () => { const r = detectProject(fake({ 'index.html': 'x', 'package.json': pkg({ build: 'vite build' }) }), {}); assert.equal(r.kind, 'build'); }); test('index.html + nur dev-Skript ⇒ static (kein build)', () => { const r = detectProject(fake({ 'index.html': 'x', 'package.json': pkg({ dev: 'vite' }) }), {}); assert.equal(r.kind, 'static'); }); test('mode dev erzwingt dev, auch mit build', () => { const r = detectProject(fake({ 'package.json': pkg({ build: 'b', dev: 'd' }) }), { mode: 'dev' }); assert.equal(r.kind, 'dev'); assert.ok(r.devCmd); }); test('mode dev ohne dev-Skript ⇒ Fehler', () => { const r = detectProject(fake({ 'package.json': pkg({ build: 'b' }) }), { mode: 'dev' }); assert.equal(r.kind, 'dev'); assert.equal(r.error, 'Kein dev-Skript in package.json'); }); test('mode build ohne build-Skript ⇒ Fehler', () => { const r = detectProject(fake({ 'index.html': 'x', 'package.json': pkg({ dev: 'd' }) }), { mode: 'build' }); assert.equal(r.kind, 'build'); assert.equal(r.error, 'Kein build-Skript in package.json'); }); test('devPort 3000 aus previewJson landet im devCmd', () => { const r = detectProject(fake({ 'package.json': pkg({ dev: 'd' }) }), { previewJson: { devPort: 3000 } }); assert.equal(r.devPort, 3000); assert.deepEqual(r.devCmd.slice(-2), ['--port', '3000']); }); test('ungültiger devPort ⇒ 5173', () => { for (const p of [80, 70000, 3000.5, '3000', null]) { assert.equal(detectProject(fake({ 'package.json': pkg({ dev: 'd' }) }), { previewJson: { devPort: p } }).devPort, 5173); } }); test('previewJson.output überschreibt, unsicher wird ignoriert', () => { const f = fake({ 'package.json': pkg({ build: 'b' }) }); assert.deepEqual(detectProject(f, { previewJson: { output: 'public/site' } }).outputDirs, ['public/site']); for (const o of ['../x', '/etc', 'a/../b', '', 5]) { assert.deepEqual(detectProject(f, { previewJson: { output: o } }).outputDirs, ['dist', 'build', 'out', 'web-build']); } }); test('index aus opts/previewJson: eigene Startseite ⇒ static', () => { const f = fake({ 'site/home.html': 'x', 'package.json': pkg({ dev: 'd' }) }); const r = detectProject(f, { previewJson: { index: 'site/home.html' } }); assert.equal(r.kind, 'static'); assert.equal(r.index, 'site/home.html'); assert.equal(detectProject(f, { index: '../evil.html' }).kind, 'dev'); }); test('kaputtes package.json wirft nicht', () => { const r = detectProject(fake({ 'package.json': '{kaputt' }), {}); assert.equal(r.kind, 'static'); assert.ok(r.hint); }); test('weder index noch Skripte ⇒ static mit Hinweis', () => { const r = detectProject(fake({ 'README.md': 'x' }), {}); assert.equal(r.kind, 'static'); assert.match(r.hint, /Hinweis|index|Startseite/i); }); test('detectFromDir liest das Dateisystem', () => { const d = mkdtempSync(join(tmpdir(), 'detect-')); writeFileSync(join(d, 'package.json'), pkg({ build: 'vite build' })); writeFileSync(join(d, 'pnpm-lock.yaml'), ''); writeFileSync(join(d, '.webklar-preview.json'), JSON.stringify({ output: 'out2' })); assert.equal(detectFromDir(d, {}).pm, 'pnpm'); assert.deepEqual(detectFromDir(d, {}).outputDirs, ['out2']); const e = mkdtempSync(join(tmpdir(), 'detect-')); mkdirSync(join(e, 'index.html')); assert.equal(detectFromDir(e, {}).kind, 'static'); assert.equal(detectFromDir(join(e, 'nix'), {}).kind, 'static'); });