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:
255
quartz/build.ts
255
quartz/build.ts
@@ -124,7 +124,12 @@ async function startWatching(
|
|||||||
|
|
||||||
for (const content of initialContent) {
|
for (const content of initialContent) {
|
||||||
const [_tree, vfile] = content
|
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",
|
type: "markdown",
|
||||||
content,
|
content,
|
||||||
})
|
})
|
||||||
@@ -165,19 +170,25 @@ async function startWatching(
|
|||||||
fp = toPosixPath(fp)
|
fp = toPosixPath(fp)
|
||||||
if (buildData.ignored(fp)) return
|
if (buildData.ignored(fp)) return
|
||||||
changes.push({ path: fp as FilePath, type: "add" })
|
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) => {
|
.on("change", (fp) => {
|
||||||
fp = toPosixPath(fp)
|
fp = toPosixPath(fp)
|
||||||
if (buildData.ignored(fp)) return
|
if (buildData.ignored(fp)) return
|
||||||
changes.push({ path: fp as FilePath, type: "change" })
|
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) => {
|
.on("unlink", (fp) => {
|
||||||
fp = toPosixPath(fp)
|
fp = toPosixPath(fp)
|
||||||
if (buildData.ignored(fp)) return
|
if (buildData.ignored(fp)) return
|
||||||
changes.push({ path: fp as FilePath, type: "delete" })
|
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 () => {
|
return async () => {
|
||||||
@@ -194,147 +205,155 @@ async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildD
|
|||||||
buildData.lastBuildMs = new Date().getTime()
|
buildData.lastBuildMs = new Date().getTime()
|
||||||
const numChangesInBuild = changes.length
|
const numChangesInBuild = changes.length
|
||||||
const release = await mut.acquire()
|
const release = await mut.acquire()
|
||||||
|
try {
|
||||||
// if there's another build after us, release and let them do it
|
// if there's another build after us, release and let them do it
|
||||||
if (ctx.buildId !== buildId) {
|
if (ctx.buildId !== buildId) {
|
||||||
release()
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const perf = new PerfTimer()
|
|
||||||
perf.addEvent("rebuild")
|
|
||||||
console.log(styleText("yellow", "Detected change, rebuilding..."))
|
|
||||||
|
|
||||||
// update changesSinceLastBuild
|
|
||||||
for (const change of changes) {
|
|
||||||
changesSinceLastBuild[change.path] = change.type
|
|
||||||
}
|
|
||||||
|
|
||||||
const staticResources = getStaticResourcesFromPlugins(ctx)
|
|
||||||
const pathsToParse: FilePath[] = []
|
|
||||||
for (const [fp, type] of Object.entries(changesSinceLastBuild)) {
|
|
||||||
if (type === "delete" || path.extname(fp) !== ".md") continue
|
|
||||||
const fullPath = joinSegments(argv.directory, toPosixPath(fp)) as FilePath
|
|
||||||
pathsToParse.push(fullPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = await parseMarkdown(ctx, pathsToParse)
|
|
||||||
for (const content of parsed) {
|
|
||||||
contentMap.set(content[1].data.relativePath!, {
|
|
||||||
type: "markdown",
|
|
||||||
content,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// update state using changesSinceLastBuild
|
|
||||||
// we do this weird play of add => compute change events => remove
|
|
||||||
// so that partialEmitters can do appropriate cleanup based on the content of deleted files
|
|
||||||
for (const [file, change] of Object.entries(changesSinceLastBuild)) {
|
|
||||||
if (change === "delete") {
|
|
||||||
// universal delete case
|
|
||||||
contentMap.delete(file as FilePath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// manually track non-markdown files as processed files only
|
const perf = new PerfTimer()
|
||||||
// contains markdown files
|
perf.addEvent("rebuild")
|
||||||
if (change === "add" && path.extname(file) !== ".md") {
|
console.log(styleText("yellow", "Detected change, rebuilding..."))
|
||||||
contentMap.set(file as FilePath, {
|
|
||||||
type: "other",
|
// update changesSinceLastBuild
|
||||||
|
for (const change of changes) {
|
||||||
|
changesSinceLastBuild[change.path] = change.type
|
||||||
|
}
|
||||||
|
|
||||||
|
const staticResources = getStaticResourcesFromPlugins(ctx)
|
||||||
|
const pathsToParse: FilePath[] = []
|
||||||
|
for (const [fp, type] of Object.entries(changesSinceLastBuild)) {
|
||||||
|
if (type === "delete" || path.extname(fp) !== ".md") continue
|
||||||
|
const fullPath = joinSegments(argv.directory, toPosixPath(fp)) as FilePath
|
||||||
|
pathsToParse.push(fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = await parseMarkdown(ctx, pathsToParse)
|
||||||
|
for (const content of parsed) {
|
||||||
|
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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const changeEvents: ChangeEvent[] = Object.entries(changesSinceLastBuild).map(([fp, type]) => {
|
// update state using changesSinceLastBuild
|
||||||
const path = fp as FilePath
|
// we do this weird play of add => compute change events => remove
|
||||||
const processedContent = contentMap.get(path)
|
// so that partialEmitters can do appropriate cleanup based on the content of deleted files
|
||||||
if (processedContent?.type === "markdown") {
|
for (const [file, change] of Object.entries(changesSinceLastBuild)) {
|
||||||
const [_tree, file] = processedContent.content
|
if (change === "delete") {
|
||||||
|
// universal delete case
|
||||||
|
contentMap.delete(file as FilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// manually track non-markdown files as processed files only
|
||||||
|
// contains markdown files
|
||||||
|
if (change === "add" && path.extname(file) !== ".md") {
|
||||||
|
contentMap.set(file as FilePath, {
|
||||||
|
type: "other",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeEvents: ChangeEvent[] = Object.entries(changesSinceLastBuild).map(([fp, type]) => {
|
||||||
|
const path = fp as FilePath
|
||||||
|
const processedContent = contentMap.get(path)
|
||||||
|
if (processedContent?.type === "markdown") {
|
||||||
|
const [_tree, file] = processedContent.content
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
path,
|
||||||
|
file,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
path,
|
path,
|
||||||
file,
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// update allFiles and then allSlugs with the consistent view of content map
|
||||||
|
ctx.allFiles = Array.from(contentMap.keys())
|
||||||
|
ctx.allSlugs = ctx.allFiles.map((fp) => slugifyFilePath(fp as FilePath))
|
||||||
|
|
||||||
|
const markdownContent = Array.from(contentMap.values())
|
||||||
|
.filter((file) => file.type === "markdown")
|
||||||
|
.map((file) => file.content)
|
||||||
|
reportSlugCollisions(markdownContent)
|
||||||
|
let processedFiles = filterContent(ctx, markdownContent)
|
||||||
|
|
||||||
|
let emittedFiles = 0
|
||||||
|
|
||||||
|
// Phase 1: Run PageTypeDispatcher first so it populates ctx.virtualPages
|
||||||
|
const dispatcher = cfg.plugins.emitters.find((e) => e.name === "PageTypeDispatcher")
|
||||||
|
if (dispatcher) {
|
||||||
|
ctx.virtualPages = []
|
||||||
|
const emitFn = dispatcher.partialEmit ?? dispatcher.emit
|
||||||
|
const emitted = await emitFn(ctx, processedFiles, staticResources, changeEvents)
|
||||||
|
if (emitted !== null) {
|
||||||
|
if (Symbol.asyncIterator in emitted) {
|
||||||
|
for await (const file of emitted) {
|
||||||
|
emittedFiles++
|
||||||
|
if (ctx.argv.verbose) {
|
||||||
|
console.log(`[emit:${dispatcher.name}] ${file}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emittedFiles += emitted.length
|
||||||
|
if (ctx.argv.verbose) {
|
||||||
|
for (const file of emitted) {
|
||||||
|
console.log(`[emit:${dispatcher.name}] ${file}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
// Phase 2: Run all other emitters with content extended by virtual pages
|
||||||
type,
|
const contentWithVirtual =
|
||||||
path,
|
ctx.virtualPages.length > 0 ? [...processedFiles, ...ctx.virtualPages] : processedFiles
|
||||||
}
|
for (const emitter of cfg.plugins.emitters) {
|
||||||
})
|
if (emitter.name === "PageTypeDispatcher") continue
|
||||||
|
// Try to use partialEmit if available, otherwise assume the output is static
|
||||||
|
const emitFn = emitter.partialEmit ?? emitter.emit
|
||||||
|
const emitted = await emitFn(ctx, contentWithVirtual, staticResources, changeEvents)
|
||||||
|
if (emitted === null) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// update allFiles and then allSlugs with the consistent view of content map
|
|
||||||
ctx.allFiles = Array.from(contentMap.keys())
|
|
||||||
ctx.allSlugs = ctx.allFiles.map((fp) => slugifyFilePath(fp as FilePath))
|
|
||||||
|
|
||||||
const markdownContent = Array.from(contentMap.values())
|
|
||||||
.filter((file) => file.type === "markdown")
|
|
||||||
.map((file) => file.content)
|
|
||||||
reportSlugCollisions(markdownContent)
|
|
||||||
let processedFiles = filterContent(ctx, markdownContent)
|
|
||||||
|
|
||||||
let emittedFiles = 0
|
|
||||||
|
|
||||||
// Phase 1: Run PageTypeDispatcher first so it populates ctx.virtualPages
|
|
||||||
const dispatcher = cfg.plugins.emitters.find((e) => e.name === "PageTypeDispatcher")
|
|
||||||
if (dispatcher) {
|
|
||||||
ctx.virtualPages = []
|
|
||||||
const emitFn = dispatcher.partialEmit ?? dispatcher.emit
|
|
||||||
const emitted = await emitFn(ctx, processedFiles, staticResources, changeEvents)
|
|
||||||
if (emitted !== null) {
|
|
||||||
if (Symbol.asyncIterator in emitted) {
|
if (Symbol.asyncIterator in emitted) {
|
||||||
|
// Async generator case
|
||||||
for await (const file of emitted) {
|
for await (const file of emitted) {
|
||||||
emittedFiles++
|
emittedFiles++
|
||||||
if (ctx.argv.verbose) {
|
if (ctx.argv.verbose) {
|
||||||
console.log(`[emit:${dispatcher.name}] ${file}`)
|
console.log(`[emit:${emitter.name}] ${file}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Array case
|
||||||
emittedFiles += emitted.length
|
emittedFiles += emitted.length
|
||||||
if (ctx.argv.verbose) {
|
if (ctx.argv.verbose) {
|
||||||
for (const file of emitted) {
|
for (const file of emitted) {
|
||||||
console.log(`[emit:${dispatcher.name}] ${file}`)
|
console.log(`[emit:${emitter.name}] ${file}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 2: Run all other emitters with content extended by virtual pages
|
|
||||||
const contentWithVirtual =
|
|
||||||
ctx.virtualPages.length > 0 ? [...processedFiles, ...ctx.virtualPages] : processedFiles
|
|
||||||
for (const emitter of cfg.plugins.emitters) {
|
|
||||||
if (emitter.name === "PageTypeDispatcher") continue
|
|
||||||
// Try to use partialEmit if available, otherwise assume the output is static
|
|
||||||
const emitFn = emitter.partialEmit ?? emitter.emit
|
|
||||||
const emitted = await emitFn(ctx, contentWithVirtual, staticResources, changeEvents)
|
|
||||||
if (emitted === null) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Symbol.asyncIterator in emitted) {
|
|
||||||
// Async generator case
|
|
||||||
for await (const file of emitted) {
|
|
||||||
emittedFiles++
|
|
||||||
if (ctx.argv.verbose) {
|
|
||||||
console.log(`[emit:${emitter.name}] ${file}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Array case
|
|
||||||
emittedFiles += emitted.length
|
|
||||||
if (ctx.argv.verbose) {
|
|
||||||
for (const file of emitted) {
|
|
||||||
console.log(`[emit:${emitter.name}] ${file}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
release()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {
|
export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {
|
||||||
|
|||||||
@@ -854,10 +854,13 @@ function resolveGroups(
|
|||||||
const groupConfig = groups[item.group]
|
const groupConfig = groups[item.group]
|
||||||
groupPriority.set(item.group, groupConfig?.priority ?? item.priority)
|
groupPriority.set(item.group, groupConfig?.priority ?? item.priority)
|
||||||
}
|
}
|
||||||
groupedComponents.get(item.group)!.push({
|
const groupMembers = groupedComponents.get(item.group)
|
||||||
component: item.component,
|
if (groupMembers) {
|
||||||
groupOptions: item.groupOptions,
|
groupMembers.push({
|
||||||
})
|
component: item.component,
|
||||||
|
groupOptions: item.groupOptions,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,7 +876,8 @@ function resolveGroups(
|
|||||||
if (processedGroups.has(item.group)) continue
|
if (processedGroups.has(item.group)) continue
|
||||||
processedGroups.add(item.group)
|
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 groupConfig = groups[item.group] ?? {}
|
||||||
|
|
||||||
const flexComponents = members.map((m) => ({
|
const flexComponents = members.map((m) => ({
|
||||||
@@ -896,7 +900,7 @@ function resolveGroups(
|
|||||||
gap: groupConfig.gap ?? "1rem",
|
gap: groupConfig.gap ?? "1rem",
|
||||||
}) as QuartzComponent
|
}) as QuartzComponent
|
||||||
|
|
||||||
entries.push({ priority: groupPriority.get(item.group)!, component: flexComponent })
|
entries.push({ priority: groupPriority.get(item.group) ?? 50, component: flexComponent })
|
||||||
} else {
|
} else {
|
||||||
entries.push({ priority: item.priority, component: item.component })
|
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(
|
const otherEmitters = cfg.plugins.emitters.filter(
|
||||||
(e) => e.name !== "PageTypeDispatcher" && e.name !== "ComponentResources",
|
(e) => e.name !== "PageTypeDispatcher" && e.name !== "ComponentResources",
|
||||||
)
|
)
|
||||||
|
let emitErrors = 0
|
||||||
const counts = await Promise.all(
|
const counts = await Promise.all(
|
||||||
otherEmitters.map((emitter) =>
|
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)
|
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()}`)
|
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,
|
maxWorkers: concurrency,
|
||||||
workerType: "thread",
|
workerType: "thread",
|
||||||
})
|
})
|
||||||
const errorHandler = (err: any) => {
|
|
||||||
console.error(err)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const serializableCtx: WorkerSerializableBuildCtx = {
|
const serializableCtx: WorkerSerializableBuildCtx = {
|
||||||
buildId: ctx.buildId,
|
buildId: ctx.buildId,
|
||||||
argv: ctx.argv,
|
argv: ctx.argv,
|
||||||
@@ -185,37 +180,40 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
|
|||||||
virtualPages: [],
|
virtualPages: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
const textToMarkdownPromises: WorkerPromise<MarkdownContent[]>[] = []
|
try {
|
||||||
let processedFiles = 0
|
const textToMarkdownPromises: WorkerPromise<MarkdownContent[]>[] = []
|
||||||
for (const chunk of chunks(fps, CHUNK_SIZE)) {
|
let processedFiles = 0
|
||||||
textToMarkdownPromises.push(pool.exec("parseMarkdown", [serializableCtx, chunk]))
|
for (const chunk of chunks(fps, CHUNK_SIZE)) {
|
||||||
|
textToMarkdownPromises.push(pool.exec("parseMarkdown", [serializableCtx, chunk]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const mdResults: Array<MarkdownContent[]> = await Promise.all(
|
||||||
|
textToMarkdownPromises.map(async (promise) => {
|
||||||
|
const result = await promise
|
||||||
|
processedFiles += result.length
|
||||||
|
log.updateText(`text->markdown ${styleText("gray", `${processedFiles}/${fps.length}`)}`)
|
||||||
|
return result
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const markdownToHtmlPromises: WorkerPromise<ProcessedContent[]>[] = []
|
||||||
|
processedFiles = 0
|
||||||
|
for (const mdChunk of mdResults) {
|
||||||
|
markdownToHtmlPromises.push(pool.exec("processHtml", [serializableCtx, mdChunk]))
|
||||||
|
}
|
||||||
|
const results: ProcessedContent[][] = await Promise.all(
|
||||||
|
markdownToHtmlPromises.map(async (promise) => {
|
||||||
|
const result = await promise
|
||||||
|
processedFiles += result.length
|
||||||
|
log.updateText(`markdown->html ${styleText("gray", `${processedFiles}/${fps.length}`)}`)
|
||||||
|
return result
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
res = results.flat()
|
||||||
|
} finally {
|
||||||
|
await pool.terminate()
|
||||||
}
|
}
|
||||||
|
|
||||||
const mdResults: Array<MarkdownContent[]> = await Promise.all(
|
|
||||||
textToMarkdownPromises.map(async (promise) => {
|
|
||||||
const result = await promise
|
|
||||||
processedFiles += result.length
|
|
||||||
log.updateText(`text->markdown ${styleText("gray", `${processedFiles}/${fps.length}`)}`)
|
|
||||||
return result
|
|
||||||
}),
|
|
||||||
).catch(errorHandler)
|
|
||||||
|
|
||||||
const markdownToHtmlPromises: WorkerPromise<ProcessedContent[]>[] = []
|
|
||||||
processedFiles = 0
|
|
||||||
for (const mdChunk of mdResults) {
|
|
||||||
markdownToHtmlPromises.push(pool.exec("processHtml", [serializableCtx, mdChunk]))
|
|
||||||
}
|
|
||||||
const results: ProcessedContent[][] = await Promise.all(
|
|
||||||
markdownToHtmlPromises.map(async (promise) => {
|
|
||||||
const result = await promise
|
|
||||||
processedFiles += result.length
|
|
||||||
log.updateText(`markdown->html ${styleText("gray", `${processedFiles}/${fps.length}`)}`)
|
|
||||||
return result
|
|
||||||
}),
|
|
||||||
).catch(errorHandler)
|
|
||||||
|
|
||||||
res = results.flat()
|
|
||||||
await pool.terminate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.end(`Parsed ${res.length} Markdown files in ${perf.timeSince()}`)
|
log.end(`Parsed ${res.length} Markdown files in ${perf.timeSince()}`)
|
||||||
|
|||||||
Reference in New Issue
Block a user