From 55991c900833ef980600358a41ef82c9c62bc36c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 13:37:34 +0000 Subject: [PATCH] previewDeploy: node_build-Autodetect fuer App-Repos + Build-Output-Erkennung - runDeploy: Repos mit package.json-build-Script ohne index.html werden automatisch als node_build gebaut (Expo/Vite/React zeigen sonst nur den Platzhalter); Build-Fehler fallen auf static+Platzhalter zurueck - deployNodeBuild: npm-ci-Fallback auf npm install, Output-Erkennung dist/build/out/web-build, maxBuffer+Timeout - updateWebsiteProject: bestehenden type/index aus .webklar-preview.json erhalten statt bei jedem Edit auf static zurueckzusetzen Co-Authored-By: Claude Fable 5 --- server/services/giteaAdmin.js | 13 +++++- server/services/previewDeploy.js | 74 +++++++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/server/services/giteaAdmin.js b/server/services/giteaAdmin.js index ced5d1a..bb908df 100644 --- a/server/services/giteaAdmin.js +++ b/server/services/giteaAdmin.js @@ -432,9 +432,20 @@ export async function updateWebsiteProject(projectId, { projectName, subdomain, /* Default main */ } + // Deploy-Typ (z.B. node_build) und index-Unterordner aus der vorhandenen + // Repo-Config uebernehmen - sonst wuerde jedes Bearbeiten im Portal + // App-Projekte (Expo/Vite/React) wieder auf "static" zuruecksetzen. + let existingCfg = null + try { + existingCfg = await fetchPreviewConfig(repoFullName, branch) + } catch { + /* keine bestehende Config -> Defaults */ + } + const previewConfig = { enabled: hasWebsite, - type: 'static', + type: existingCfg?.type || 'static', + ...(existingCfg?.index ? { index: existingCfg.index } : {}), branch, displayName: nextName, templateName: doc.templateName || config.gitea.templateRepo, diff --git a/server/services/previewDeploy.js b/server/services/previewDeploy.js index 825d9cb..0d73df4 100644 --- a/server/services/previewDeploy.js +++ b/server/services/previewDeploy.js @@ -215,11 +215,60 @@ async function ensureIndexHtml(targetDir, displayName) { return { placeholder: true } } +/** + * Erkennt App-Repos, die einen Build-Schritt brauchen (Expo, Vite, React ...): + * package.json mit "build"-Script vorhanden, aber keine index.html im Webroot. + * Solche Repos wuerden als "static" nur den Platzhalter zeigen - deshalb wird + * der Deploy-Typ automatisch auf node_build angehoben (Langzeit-Fix, damit + * App-Projekte ohne manuelle Config-Pflege korrekt angezeigt werden). + */ +async function shouldAutoBuild(sourceDir, indexSubdir = '') { + try { + const pkg = JSON.parse(await fs.readFile(path.join(sourceDir, 'package.json'), 'utf8')) + if (!pkg?.scripts?.build) return false + } catch { + return false + } + const webroot = indexSubdir ? path.join(sourceDir, indexSubdir) : sourceDir + try { + await fs.access(path.join(webroot, 'index.html')) + return false // echte statische Startseite vorhanden -> kein Build noetig + } catch { + return true + } +} + +/** + * npm ci + npm run build, dann das Build-Ausgabeverzeichnis deployen. + * Ausgabeverzeichnis wird erkannt (dist > build > out > web-build), weil + * Toolchains unterschiedliche Defaults haben (Expo/Vite: dist, CRA: build). + */ async function deployNodeBuild(sourceDir, targetDir) { - await execFileAsync('npm', ['ci'], { cwd: sourceDir }) - await execFileAsync('npm', ['run', 'build'], { cwd: sourceDir }) - const distDir = path.join(sourceDir, 'dist') - await deployStatic(distDir, targetDir) + const execOpts = { + cwd: sourceDir, + maxBuffer: 64 * 1024 * 1024, + timeout: 15 * 60 * 1000, + env: { ...process.env, CI: '1' }, + } + try { + await execFileAsync('npm', ['ci'], execOpts) + } catch { + // Ohne (passendes) package-lock.json schlaegt npm ci fehl -> npm install. + await execFileAsync('npm', ['install', '--no-audit', '--no-fund'], execOpts) + } + await execFileAsync('npm', ['run', 'build'], execOpts) + + for (const dirName of ['dist', 'build', 'out', 'web-build']) { + const candidate = path.join(sourceDir, dirName) + try { + await fs.access(path.join(candidate, 'index.html')) + await deployStatic(candidate, targetDir) + return + } catch { + /* naechsten Kandidaten pruefen */ + } + } + throw new Error('Build erzeugte kein Ausgabeverzeichnis mit index.html (dist/build/out/web-build)') } export function buildPreviewUrl(subdomain) { @@ -310,8 +359,21 @@ export async function runDeploy(repoFullName, branch, previewConfig) { await cloneRepo(cloneUrl, workDir, branch, config.gitea.apiToken) - if (previewConfig.type === 'node_build') { - await deployNodeBuild(workDir, targetDir) + let deployType = previewConfig.type + if (deployType !== 'node_build' && (await shouldAutoBuild(workDir, previewConfig.index || ''))) { + console.log(`[previewDeploy] ${repoFullName}: package.json mit build-Script, keine index.html -> node_build (auto)`) + deployType = 'node_build' + } + + if (deployType === 'node_build') { + try { + await deployNodeBuild(workDir, targetDir) + } catch (err) { + // Build-Fehler duerfen die Subdomain nicht toeten: statisch deployen, + // ensureIndexHtml haelt sie mit Kandidat/Platzhalter erreichbar. + console.error(`[previewDeploy] node_build fuer ${repoFullName} fehlgeschlagen, Fallback auf static:`, err.message) + await deployStatic(workDir, targetDir, previewConfig.index || '') + } } else { await deployStatic(workDir, targetDir, previewConfig.index || '') }