From ec00a40aefca73596ab76e3ebe3a8e1129b43688 Mon Sep 17 00:00:00 2001
From: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 27 Jan 2026 18:27:17 +0000
Subject: [PATCH] chore(deps): bump the production-dependencies group with 4 updates (#2289)
---
quartz/build.ts | 333 +++++++++++++++++++++++++++++++++++++++++++++++--------
1 files changed, 285 insertions(+), 48 deletions(-)
diff --git a/quartz/build.ts b/quartz/build.ts
index db8e4a9..b98f4a8 100644
--- a/quartz/build.ts
+++ b/quartz/build.ts
@@ -1,69 +1,306 @@
+import sourceMapSupport from "source-map-support"
+sourceMapSupport.install(options)
import path from "path"
-import { PerfTimer } from "./perf"
-import { rimraf } from "rimraf"
-import { globby } from "globby"
-import chalk from "chalk"
-import http from "http"
-import serveHandler from "serve-handler"
+import { PerfTimer } from "./util/perf"
+import { rm } from "fs/promises"
+import { GlobbyFilterFunction, isGitIgnored } from "globby"
+import { styleText } from "util"
import { parseMarkdown } from "./processors/parse"
import { filterContent } from "./processors/filter"
import { emitContent } from "./processors/emit"
import cfg from "../quartz.config"
+import { FilePath, joinSegments, slugifyFilePath } from "./util/path"
+import chokidar from "chokidar"
+import { ProcessedContent } from "./plugins/vfile"
+import { Argv, BuildCtx } from "./util/ctx"
+import { glob, toPosixPath } from "./util/glob"
+import { trace } from "./util/trace"
+import { options } from "./util/sourcemap"
+import { Mutex } from "async-mutex"
+import { getStaticResourcesFromPlugins } from "./plugins"
+import { randomIdNonSecure } from "./util/random"
+import { ChangeEvent } from "./plugins/types"
+import { minimatch } from "minimatch"
-interface Argv {
- directory: string
- verbose: boolean
- output: string
- clean: boolean
- serve: boolean
- port: number
+type ContentMap = Map<
+ FilePath,
+ | {
+ type: "markdown"
+ content: ProcessedContent
+ }
+ | {
+ type: "other"
+ }
+>
+
+type BuildData = {
+ ctx: BuildCtx
+ ignored: GlobbyFilterFunction
+ mut: Mutex
+ contentMap: ContentMap
+ changesSinceLastBuild: Record<FilePath, ChangeEvent["type"]>
+ lastBuildMs: number
}
-export default async function buildQuartz(argv: Argv, version: string) {
- console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
+async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
+ const ctx: BuildCtx = {
+ buildId: randomIdNonSecure(),
+ argv,
+ cfg,
+ allSlugs: [],
+ allFiles: [],
+ incremental: false,
+ }
+
const perf = new PerfTimer()
const output = argv.output
const pluginCount = Object.values(cfg.plugins).flat().length
- const pluginNames = (key: 'transformers' | 'filters' | 'emitters') => cfg.plugins[key].map(plugin => plugin.name)
- console.log(`Loaded ${pluginCount} plugins`)
+ const pluginNames = (key: "transformers" | "filters" | "emitters") =>
+ cfg.plugins[key].map((plugin) => plugin.name)
if (argv.verbose) {
- console.log(` Transformers: ${pluginNames('transformers').join(", ")}`)
- console.log(` Filters: ${pluginNames('filters').join(", ")}`)
- console.log(` Emitters: ${pluginNames('emitters').join(", ")}`)
+ console.log(`Loaded ${pluginCount} plugins`)
+ console.log(` Transformers: ${pluginNames("transformers").join(", ")}`)
+ console.log(` Filters: ${pluginNames("filters").join(", ")}`)
+ console.log(` Emitters: ${pluginNames("emitters").join(", ")}`)
}
- // clean
- if (argv.clean) {
- perf.addEvent('clean')
- await rimraf(output)
- console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince('clean')}`)
+ const release = await mut.acquire()
+ perf.addEvent("clean")
+ await rm(output, { recursive: true, force: true })
+ console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`)
+
+ perf.addEvent("glob")
+ const allFiles = await glob("**/*.*", argv.directory, cfg.configuration.ignorePatterns)
+ const markdownPaths = allFiles.filter((fp) => fp.endsWith(".md")).sort()
+ console.log(
+ `Found ${markdownPaths.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`,
+ )
+
+ const filePaths = markdownPaths.map((fp) => joinSegments(argv.directory, fp) as FilePath)
+ ctx.allFiles = allFiles
+ ctx.allSlugs = allFiles.map((fp) => slugifyFilePath(fp as FilePath))
+
+ const parsedFiles = await parseMarkdown(ctx, filePaths)
+ const filteredContent = filterContent(ctx, parsedFiles)
+
+ await emitContent(ctx, filteredContent)
+ console.log(
+ styleText("green", `Done processing ${markdownPaths.length} files in ${perf.timeSince()}`),
+ )
+ release()
+
+ if (argv.watch) {
+ ctx.incremental = true
+ return startWatching(ctx, mut, parsedFiles, clientRefresh)
}
+}
- // glob
- perf.addEvent('glob')
- const fps = await globby('**/*.md', {
- cwd: argv.directory,
- ignore: cfg.configuration.ignorePatterns,
- gitignore: true,
- })
- console.log(`Found ${fps.length} input files in ${perf.timeSince('glob')}`)
+// setup watcher for rebuilds
+async function startWatching(
+ ctx: BuildCtx,
+ mut: Mutex,
+ initialContent: ProcessedContent[],
+ clientRefresh: () => void,
+) {
+ const { argv, allFiles } = ctx
- const filePaths = fps.map(fp => `${argv.directory}${path.sep}${fp}`)
- const parsedFiles = await parseMarkdown(cfg.plugins.transformers, argv.directory, filePaths, argv.verbose)
- const filteredContent = filterContent(cfg.plugins.filters, parsedFiles, argv.verbose)
- await emitContent(argv.directory, output, cfg, filteredContent, argv.verbose)
- console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
-
- if (argv.serve) {
- const server = http.createServer(async (req, res) => {
- return serveHandler(req, res, {
- public: output,
- directoryListing: false
- })
+ const contentMap: ContentMap = new Map()
+ for (const filePath of allFiles) {
+ contentMap.set(filePath, {
+ type: "other",
})
- server.listen(argv.port)
- console.log(`Started a Quartz server listening at http://localhost:${argv.port}`)
- console.log('hint: exit with ctrl+c')
+ }
+
+ for (const content of initialContent) {
+ const [_tree, vfile] = content
+ contentMap.set(vfile.data.relativePath!, {
+ type: "markdown",
+ content,
+ })
+ }
+
+ const gitIgnoredMatcher = await isGitIgnored()
+ const buildData: BuildData = {
+ ctx,
+ mut,
+ contentMap,
+ ignored: (fp) => {
+ const pathStr = toPosixPath(fp.toString())
+ if (pathStr.startsWith(".git/")) return true
+ if (gitIgnoredMatcher(pathStr)) return true
+ for (const pattern of cfg.configuration.ignorePatterns) {
+ if (minimatch(pathStr, pattern)) {
+ return true
+ }
+ }
+
+ return false
+ },
+
+ changesSinceLastBuild: {},
+ lastBuildMs: 0,
+ }
+
+ const watcher = chokidar.watch(".", {
+ awaitWriteFinish: { stabilityThreshold: 250 },
+ persistent: true,
+ cwd: argv.directory,
+ ignoreInitial: true,
+ })
+
+ const changes: ChangeEvent[] = []
+ watcher
+ .on("add", (fp) => {
+ fp = toPosixPath(fp)
+ if (buildData.ignored(fp)) return
+ changes.push({ path: fp as FilePath, type: "add" })
+ void rebuild(changes, clientRefresh, buildData)
+ })
+ .on("change", (fp) => {
+ fp = toPosixPath(fp)
+ if (buildData.ignored(fp)) return
+ changes.push({ path: fp as FilePath, type: "change" })
+ void rebuild(changes, clientRefresh, buildData)
+ })
+ .on("unlink", (fp) => {
+ fp = toPosixPath(fp)
+ if (buildData.ignored(fp)) return
+ changes.push({ path: fp as FilePath, type: "delete" })
+ void rebuild(changes, clientRefresh, buildData)
+ })
+
+ return async () => {
+ await watcher.close()
+ }
+}
+
+async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildData: BuildData) {
+ const { ctx, contentMap, mut, changesSinceLastBuild } = buildData
+ const { argv, cfg } = ctx
+
+ const buildId = randomIdNonSecure()
+ ctx.buildId = buildId
+ buildData.lastBuildMs = new Date().getTime()
+ const numChangesInBuild = changes.length
+ const release = await mut.acquire()
+
+ // if there's another build after us, release and let them do it
+ if (ctx.buildId !== buildId) {
+ release()
+ 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
+ // 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 {
+ type,
+ path,
+ }
+ })
+
+ // 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))
+ let processedFiles = filterContent(
+ ctx,
+ Array.from(contentMap.values())
+ .filter((file) => file.type === "markdown")
+ .map((file) => file.content),
+ )
+
+ let emittedFiles = 0
+ for (const emitter of cfg.plugins.emitters) {
+ // Try to use partialEmit if available, otherwise assume the output is static
+ const emitFn = emitter.partialEmit ?? emitter.emit
+ const emitted = await emitFn(ctx, processedFiles, 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) => {
+ try {
+ return await buildQuartz(argv, mut, clientRefresh)
+ } catch (err) {
+ trace("\nExiting Quartz due to a fatal error", err as Error)
}
}
--
Gitblit v1.10.0