fix: harden core build system against silent failures
- Wrap rebuild() in try/finally to guarantee mutex release (prevents deadlock) - Surface rebuild errors via .catch() instead of discarding with void - Add null checks for vfile.data.relativePath (prevents crash on virtual files) - Add try-finally for worker pool cleanup in parse.ts (prevents thread leaks) - Remove process.exit(1) from parse error handler (let errors propagate) - Add per-emitter error handling in emit phase with degraded-build warning - Replace unsafe Map.get()! assertions with null-checked access in config-loader
This commit is contained in:
@@ -124,7 +124,12 @@ async function startWatching(
|
||||
|
||||
for (const content of initialContent) {
|
||||
const [_tree, vfile] = content
|
||||
contentMap.set(vfile.data.relativePath!, {
|
||||
const relPath = vfile.data.relativePath
|
||||
if (!relPath) {
|
||||
console.warn(`Skipping file with no relativePath: ${vfile.path}`)
|
||||
continue
|
||||
}
|
||||
contentMap.set(relPath, {
|
||||
type: "markdown",
|
||||
content,
|
||||
})
|
||||
@@ -165,19 +170,25 @@ async function startWatching(
|
||||
fp = toPosixPath(fp)
|
||||
if (buildData.ignored(fp)) return
|
||||
changes.push({ path: fp as FilePath, type: "add" })
|
||||
void rebuild(changes, clientRefresh, buildData)
|
||||
rebuild(changes, clientRefresh, buildData).catch((err) => {
|
||||
console.error(styleText("red", "Rebuild failed:"), err.message ?? err)
|
||||
})
|
||||
})
|
||||
.on("change", (fp) => {
|
||||
fp = toPosixPath(fp)
|
||||
if (buildData.ignored(fp)) return
|
||||
changes.push({ path: fp as FilePath, type: "change" })
|
||||
void rebuild(changes, clientRefresh, buildData)
|
||||
rebuild(changes, clientRefresh, buildData).catch((err) => {
|
||||
console.error(styleText("red", "Rebuild failed:"), err.message ?? err)
|
||||
})
|
||||
})
|
||||
.on("unlink", (fp) => {
|
||||
fp = toPosixPath(fp)
|
||||
if (buildData.ignored(fp)) return
|
||||
changes.push({ path: fp as FilePath, type: "delete" })
|
||||
void rebuild(changes, clientRefresh, buildData)
|
||||
rebuild(changes, clientRefresh, buildData).catch((err) => {
|
||||
console.error(styleText("red", "Rebuild failed:"), err.message ?? err)
|
||||
})
|
||||
})
|
||||
|
||||
return async () => {
|
||||
@@ -194,10 +205,9 @@ async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildD
|
||||
buildData.lastBuildMs = new Date().getTime()
|
||||
const numChangesInBuild = changes.length
|
||||
const release = await mut.acquire()
|
||||
|
||||
try {
|
||||
// if there's another build after us, release and let them do it
|
||||
if (ctx.buildId !== buildId) {
|
||||
release()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -220,7 +230,12 @@ async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildD
|
||||
|
||||
const parsed = await parseMarkdown(ctx, pathsToParse)
|
||||
for (const content of parsed) {
|
||||
contentMap.set(content[1].data.relativePath!, {
|
||||
const relPath = content[1].data.relativePath
|
||||
if (!relPath) {
|
||||
console.warn(`Skipping file with no relativePath: ${content[1].path}`)
|
||||
continue
|
||||
}
|
||||
contentMap.set(relPath, {
|
||||
type: "markdown",
|
||||
content,
|
||||
})
|
||||
@@ -330,11 +345,15 @@ async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildD
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`)
|
||||
console.log(
|
||||
`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`,
|
||||
)
|
||||
console.log(styleText("green", `Done rebuilding in ${perf.timeSince()}`))
|
||||
changes.splice(0, numChangesInBuild)
|
||||
clientRefresh()
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {
|
||||
|
||||
@@ -854,12 +854,15 @@ function resolveGroups(
|
||||
const groupConfig = groups[item.group]
|
||||
groupPriority.set(item.group, groupConfig?.priority ?? item.priority)
|
||||
}
|
||||
groupedComponents.get(item.group)!.push({
|
||||
const groupMembers = groupedComponents.get(item.group)
|
||||
if (groupMembers) {
|
||||
groupMembers.push({
|
||||
component: item.component,
|
||||
groupOptions: item.groupOptions,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build a unified list of renderable entries (ungrouped components + flex groups),
|
||||
// each with a priority, so we can sort them together.
|
||||
@@ -873,7 +876,8 @@ function resolveGroups(
|
||||
if (processedGroups.has(item.group)) continue
|
||||
processedGroups.add(item.group)
|
||||
|
||||
const members = groupedComponents.get(item.group)!
|
||||
const members = groupedComponents.get(item.group)
|
||||
if (!members) continue
|
||||
const groupConfig = groups[item.group] ?? {}
|
||||
|
||||
const flexComponents = members.map((m) => ({
|
||||
@@ -896,7 +900,7 @@ function resolveGroups(
|
||||
gap: groupConfig.gap ?? "1rem",
|
||||
}) as QuartzComponent
|
||||
|
||||
entries.push({ priority: groupPriority.get(item.group)!, component: flexComponent })
|
||||
entries.push({ priority: groupPriority.get(item.group) ?? 50, component: flexComponent })
|
||||
} else {
|
||||
entries.push({ priority: item.priority, component: item.component })
|
||||
}
|
||||
|
||||
@@ -78,12 +78,23 @@ export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
|
||||
const otherEmitters = cfg.plugins.emitters.filter(
|
||||
(e) => e.name !== "PageTypeDispatcher" && e.name !== "ComponentResources",
|
||||
)
|
||||
let emitErrors = 0
|
||||
const counts = await Promise.all(
|
||||
otherEmitters.map((emitter) =>
|
||||
runEmitter(emitter, ctx, contentWithVirtual, staticResources, log),
|
||||
runEmitter(emitter, ctx, contentWithVirtual, staticResources, log).catch((err) => {
|
||||
emitErrors++
|
||||
console.error(`Emitter "${emitter.name}" failed:`, err.message ?? err)
|
||||
return 0
|
||||
}),
|
||||
),
|
||||
)
|
||||
emittedFiles += counts.reduce((sum, c) => sum + c, 0)
|
||||
|
||||
if (emitErrors > 0) {
|
||||
console.warn(
|
||||
`\nBuild completed with ${emitErrors} emitter failure(s). Output may be incomplete.`,
|
||||
)
|
||||
}
|
||||
|
||||
log.end(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince()}`)
|
||||
}
|
||||
|
||||
@@ -171,11 +171,6 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
|
||||
maxWorkers: concurrency,
|
||||
workerType: "thread",
|
||||
})
|
||||
const errorHandler = (err: any) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const serializableCtx: WorkerSerializableBuildCtx = {
|
||||
buildId: ctx.buildId,
|
||||
argv: ctx.argv,
|
||||
@@ -185,6 +180,7 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
|
||||
virtualPages: [],
|
||||
}
|
||||
|
||||
try {
|
||||
const textToMarkdownPromises: WorkerPromise<MarkdownContent[]>[] = []
|
||||
let processedFiles = 0
|
||||
for (const chunk of chunks(fps, CHUNK_SIZE)) {
|
||||
@@ -198,7 +194,7 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
|
||||
log.updateText(`text->markdown ${styleText("gray", `${processedFiles}/${fps.length}`)}`)
|
||||
return result
|
||||
}),
|
||||
).catch(errorHandler)
|
||||
)
|
||||
|
||||
const markdownToHtmlPromises: WorkerPromise<ProcessedContent[]>[] = []
|
||||
processedFiles = 0
|
||||
@@ -212,11 +208,13 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
|
||||
log.updateText(`markdown->html ${styleText("gray", `${processedFiles}/${fps.length}`)}`)
|
||||
return result
|
||||
}),
|
||||
).catch(errorHandler)
|
||||
)
|
||||
|
||||
res = results.flat()
|
||||
} finally {
|
||||
await pool.terminate()
|
||||
}
|
||||
}
|
||||
|
||||
log.end(`Parsed ${res.length} Markdown files in ${perf.timeSince()}`)
|
||||
return res
|
||||
|
||||
Reference in New Issue
Block a user