Skip to content

Commit af6c458

Browse files
committed
Add build wrapper script for better error logging on Render
1 parent 465e9bc commit af6c458

File tree

3 files changed

+248
-1
lines changed

3 files changed

+248
-1
lines changed

web/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
# next.js
1212
/.next/
13+
build.log
1314
/out/
1415
/.contentlayer/
1516

web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"scripts": {
1313
"dev": "next dev",
14-
"build": "next build 2>&1 | sed '/Contentlayer esbuild warnings:/,/^]/d' && bun run scripts/prebuild-agents-cache.ts",
14+
"build": "bun run scripts/build.ts",
1515
"start": "next start",
1616
"preview": "bun run build && bun run start",
1717
"contentlayer": "contentlayer build",

web/scripts/build.ts

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* Build wrapper script that provides detailed logging for build failures.
4+
*
5+
* Features:
6+
* - Captures all build output to build.log for debugging
7+
* - Filters noisy Contentlayer esbuild warnings from display (but keeps in log)
8+
* - Shows timing and memory usage
9+
* - On failure: displays full log for debugging
10+
* - On success: runs prebuild-agents-cache validation
11+
*/
12+
13+
import { spawn } from 'bun'
14+
import { appendFile, unlink, readFile } from 'fs/promises'
15+
import { existsSync } from 'fs'
16+
import path from 'path'
17+
18+
const LOG_FILE = path.join(import.meta.dir, '..', 'build.log')
19+
20+
// Pattern to detect Contentlayer esbuild warnings block
21+
const CONTENTLAYER_WARNING_START = /Contentlayer esbuild warnings:/
22+
const CONTENTLAYER_WARNING_END = /^\]/
23+
24+
async function clearLog() {
25+
if (existsSync(LOG_FILE)) {
26+
await unlink(LOG_FILE)
27+
}
28+
}
29+
30+
async function log(message: string) {
31+
const timestamp = new Date().toISOString()
32+
const line = `[${timestamp}] ${message}\n`
33+
await appendFile(LOG_FILE, line)
34+
}
35+
36+
async function logRaw(data: string) {
37+
await appendFile(LOG_FILE, data)
38+
}
39+
40+
function formatMemory(bytes: number): string {
41+
const mb = bytes / 1024 / 1024
42+
return `${mb.toFixed(1)}MB`
43+
}
44+
45+
function formatDuration(ms: number): string {
46+
const seconds = ms / 1000
47+
if (seconds < 60) {
48+
return `${seconds.toFixed(1)}s`
49+
}
50+
const minutes = Math.floor(seconds / 60)
51+
const remainingSeconds = seconds % 60
52+
return `${minutes}m ${remainingSeconds.toFixed(1)}s`
53+
}
54+
55+
async function runNextBuild(): Promise<number> {
56+
await log('Starting Next.js build...')
57+
await log(`Working directory: ${process.cwd()}`)
58+
await log(`Node version: ${process.version}`)
59+
await log(`Bun version: ${Bun.version}`)
60+
await log('---')
61+
62+
const startTime = Date.now()
63+
const startMemory = process.memoryUsage().heapUsed
64+
65+
const proc = spawn(['bun', 'next', 'build'], {
66+
cwd: path.join(import.meta.dir, '..'),
67+
stdout: 'pipe',
68+
stderr: 'pipe',
69+
env: {
70+
...process.env,
71+
// Force color output for better logs
72+
FORCE_COLOR: '1',
73+
},
74+
})
75+
76+
// State for filtering Contentlayer warnings
77+
let inContentlayerWarningBlock = false
78+
79+
async function processLine(line: string, isStderr: boolean) {
80+
// Always log everything to the file
81+
await logRaw(line + '\n')
82+
83+
// Check if we're entering or exiting the Contentlayer warning block
84+
if (CONTENTLAYER_WARNING_START.test(line)) {
85+
inContentlayerWarningBlock = true
86+
return // Don't print to console
87+
}
88+
89+
if (inContentlayerWarningBlock) {
90+
if (CONTENTLAYER_WARNING_END.test(line)) {
91+
inContentlayerWarningBlock = false
92+
}
93+
return // Don't print to console while in the block
94+
}
95+
96+
// Print to console (stderr goes to stderr, stdout to stdout)
97+
if (isStderr) {
98+
process.stderr.write(line + '\n')
99+
} else {
100+
process.stdout.write(line + '\n')
101+
}
102+
}
103+
104+
async function processStream(
105+
stream: ReadableStream<Uint8Array>,
106+
isStderr: boolean,
107+
) {
108+
const reader = stream.getReader()
109+
const decoder = new TextDecoder()
110+
let buffer = ''
111+
112+
try {
113+
while (true) {
114+
const { done, value } = await reader.read()
115+
if (done) break
116+
117+
buffer += decoder.decode(value, { stream: true })
118+
119+
// Process complete lines
120+
const lines = buffer.split('\n')
121+
buffer = lines.pop() || '' // Keep incomplete line in buffer
122+
123+
for (const line of lines) {
124+
await processLine(line, isStderr)
125+
}
126+
}
127+
128+
// Process any remaining content
129+
if (buffer) {
130+
await processLine(buffer, isStderr)
131+
}
132+
} finally {
133+
reader.releaseLock()
134+
}
135+
}
136+
137+
// Process both streams concurrently
138+
await Promise.all([
139+
processStream(proc.stdout, false),
140+
processStream(proc.stderr, true),
141+
])
142+
143+
const exitCode = await proc.exited
144+
const duration = Date.now() - startTime
145+
const endMemory = process.memoryUsage().heapUsed
146+
147+
await log('---')
148+
await log(`Build completed with exit code: ${exitCode}`)
149+
await log(`Duration: ${formatDuration(duration)}`)
150+
await log(`Memory used: ${formatMemory(endMemory - startMemory)}`)
151+
await log(`Peak heap: ${formatMemory(endMemory)}`)
152+
153+
console.log('')
154+
console.log(`Build duration: ${formatDuration(duration)}`)
155+
console.log(`Memory: ${formatMemory(endMemory)}`)
156+
157+
return exitCode
158+
}
159+
160+
async function runPrebuildAgentsCache(): Promise<number> {
161+
console.log('')
162+
console.log('Running prebuild agents cache validation...')
163+
await log('---')
164+
await log('Running prebuild-agents-cache.ts...')
165+
166+
const proc = spawn(['bun', 'run', 'scripts/prebuild-agents-cache.ts'], {
167+
cwd: path.join(import.meta.dir, '..'),
168+
stdout: 'inherit',
169+
stderr: 'inherit',
170+
})
171+
172+
const exitCode = await proc.exited
173+
await log(`Prebuild agents cache completed with exit code: ${exitCode}`)
174+
175+
return exitCode
176+
}
177+
178+
async function showBuildLog() {
179+
console.log('')
180+
console.log('═'.repeat(60))
181+
console.log('FULL BUILD LOG (for debugging):')
182+
console.log('═'.repeat(60))
183+
console.log('')
184+
185+
try {
186+
const logContent = await readFile(LOG_FILE, 'utf-8')
187+
console.log(logContent)
188+
} catch (error) {
189+
console.log('(Could not read build log)')
190+
}
191+
192+
console.log('')
193+
console.log('═'.repeat(60))
194+
console.log(`Log file saved to: ${LOG_FILE}`)
195+
console.log('═'.repeat(60))
196+
}
197+
198+
async function main() {
199+
console.log('Codebuff Web Build')
200+
console.log('─'.repeat(40))
201+
202+
await clearLog()
203+
await log('=== BUILD STARTED ===')
204+
await log(`Timestamp: ${new Date().toISOString()}`)
205+
206+
// Run Next.js build
207+
const buildExitCode = await runNextBuild()
208+
209+
if (buildExitCode !== 0) {
210+
console.log('')
211+
console.log('BUILD FAILED')
212+
console.log('')
213+
214+
// Show the full log on failure for debugging
215+
await showBuildLog()
216+
217+
process.exit(buildExitCode)
218+
}
219+
220+
console.log('')
221+
console.log('Next.js build succeeded')
222+
223+
// Run prebuild agents cache
224+
const cacheExitCode = await runPrebuildAgentsCache()
225+
226+
if (cacheExitCode !== 0) {
227+
console.log('')
228+
console.log('Prebuild agents cache validation failed (non-fatal)')
229+
// Don't fail the build - prebuild-agents-cache is non-fatal
230+
}
231+
232+
await log('=== BUILD COMPLETED ===')
233+
234+
console.log('')
235+
console.log('Build completed successfully!')
236+
console.log(`Build log: ${LOG_FILE}`)
237+
238+
process.exit(0)
239+
}
240+
241+
main().catch(async (error) => {
242+
console.error('Build script error:', error)
243+
await log(`Build script error: ${error}`)
244+
await showBuildLog()
245+
process.exit(1)
246+
})

0 commit comments

Comments
 (0)