Skip to content

Commit

Permalink
Merge branch 'v4' of https://github.com/jackyzha0/quartz into v4
Browse files Browse the repository at this point in the history
  • Loading branch information
Darakuu committed Feb 10, 2024
2 parents 9ce5ff7 + fe353d9 commit 823cde5
Show file tree
Hide file tree
Showing 31 changed files with 900 additions and 40 deletions.
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ This part of the configuration concerns anything that can affect the whole site.
- `ignorePatterns`: a list of [glob](<https://en.wikipedia.org/wiki/Glob_(programming)>) patterns that Quartz should ignore and not search through when looking for files inside the `content` folder. See [[private pages]] for more details.
- `defaultDateType`: whether to use created, modified, or published as the default date to display on pages and page listings.
- `theme`: configure how the site looks.
- `cdnCaching`: Whether to use Google CDN to cache the fonts (generally will be faster). Disable this if you want Quartz to be self-contained. Default to `true`
- `typography`: what fonts to use. Any font available on [Google Fonts](https://fonts.google.com/) works here.
- `header`: Font to use for headers
- `code`: Font for inline and block quotes.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"docs": "npx quartz build --serve -d docs",
"check": "tsc --noEmit && npx prettier . --check",
"format": "npx prettier . --write",
"test": "tsx ./quartz/util/path.test.ts",
"test": "tsx ./quartz/util/path.test.ts && tsx ./quartz/depgraph.test.ts",
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1"
},
"engines": {
Expand Down
1 change: 1 addition & 0 deletions quartz.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const config: QuartzConfig = {
ignorePatterns: ["private", "templates", ".obsidian"],
defaultDateType: "created",
theme: {
cdnCaching: true,
typography: {
header: "Schibsted Grotesk",
body: "Source Sans Pro",
Expand Down
194 changes: 189 additions & 5 deletions quartz/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import { glob, toPosixPath } from "./util/glob"
import { trace } from "./util/trace"
import { options } from "./util/sourcemap"
import { Mutex } from "async-mutex"
import DepGraph from "./depgraph"
import { getStaticResourcesFromPlugins } from "./plugins"

type Dependencies = Record<string, DepGraph<FilePath> | null>

type BuildData = {
ctx: BuildCtx
Expand All @@ -29,8 +33,11 @@ type BuildData = {
toRebuild: Set<FilePath>
toRemove: Set<FilePath>
lastBuildMs: number
dependencies: Dependencies
}

type FileEvent = "add" | "change" | "delete"

async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
const ctx: BuildCtx = {
argv,
Expand Down Expand Up @@ -68,12 +75,24 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {

const parsedFiles = await parseMarkdown(ctx, filePaths)
const filteredContent = filterContent(ctx, parsedFiles)

const dependencies: Record<string, DepGraph<FilePath> | null> = {}

// Only build dependency graphs if we're doing a fast rebuild
if (argv.fastRebuild) {
const staticResources = getStaticResourcesFromPlugins(ctx)
for (const emitter of cfg.plugins.emitters) {
dependencies[emitter.name] =
(await emitter.getDependencyGraph?.(ctx, filteredContent, staticResources)) ?? null
}
}

await emitContent(ctx, filteredContent)
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
release()

if (argv.serve) {
return startServing(ctx, mut, parsedFiles, clientRefresh)
return startServing(ctx, mut, parsedFiles, clientRefresh, dependencies)
}
}

Expand All @@ -83,9 +102,11 @@ async function startServing(
mut: Mutex,
initialContent: ProcessedContent[],
clientRefresh: () => void,
dependencies: Dependencies, // emitter name: dep graph
) {
const { argv } = ctx

// cache file parse results
const contentMap = new Map<FilePath, ProcessedContent>()
for (const content of initialContent) {
const [_tree, vfile] = content
Expand All @@ -95,6 +116,7 @@ async function startServing(
const buildData: BuildData = {
ctx,
mut,
dependencies,
contentMap,
ignored: await isGitIgnored(),
initialSlugs: ctx.allSlugs,
Expand All @@ -110,19 +132,181 @@ async function startServing(
ignoreInitial: true,
})

const buildFromEntry = argv.fastRebuild ? partialRebuildFromEntrypoint : rebuildFromEntrypoint
watcher
.on("add", (fp) => rebuildFromEntrypoint(fp, "add", clientRefresh, buildData))
.on("change", (fp) => rebuildFromEntrypoint(fp, "change", clientRefresh, buildData))
.on("unlink", (fp) => rebuildFromEntrypoint(fp, "delete", clientRefresh, buildData))
.on("add", (fp) => buildFromEntry(fp, "add", clientRefresh, buildData))
.on("change", (fp) => buildFromEntry(fp, "change", clientRefresh, buildData))
.on("unlink", (fp) => buildFromEntry(fp, "delete", clientRefresh, buildData))

return async () => {
await watcher.close()
}
}

async function partialRebuildFromEntrypoint(
filepath: string,
action: FileEvent,
clientRefresh: () => void,
buildData: BuildData, // note: this function mutates buildData
) {
const { ctx, ignored, dependencies, contentMap, mut, toRemove } = buildData
const { argv, cfg } = ctx

// don't do anything for gitignored files
if (ignored(filepath)) {
return
}

const buildStart = new Date().getTime()
buildData.lastBuildMs = buildStart
const release = await mut.acquire()
if (buildData.lastBuildMs > buildStart) {
release()
return
}

const perf = new PerfTimer()
console.log(chalk.yellow("Detected change, rebuilding..."))

// UPDATE DEP GRAPH
const fp = joinSegments(argv.directory, toPosixPath(filepath)) as FilePath

const staticResources = getStaticResourcesFromPlugins(ctx)
let processedFiles: ProcessedContent[] = []

switch (action) {
case "add":
// add to cache when new file is added
processedFiles = await parseMarkdown(ctx, [fp])
processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile]))

// update the dep graph by asking all emitters whether they depend on this file
for (const emitter of cfg.plugins.emitters) {
const emitterGraph =
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null

// emmiter may not define a dependency graph. nothing to update if so
if (emitterGraph) {
dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp)
}
}
break
case "change":
// invalidate cache when file is changed
processedFiles = await parseMarkdown(ctx, [fp])
processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile]))

// only content files can have added/removed dependencies because of transclusions
if (path.extname(fp) === ".md") {
for (const emitter of cfg.plugins.emitters) {
// get new dependencies from all emitters for this file
const emitterGraph =
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null

// emmiter may not define a dependency graph. nothing to update if so
if (emitterGraph) {
// merge the new dependencies into the dep graph
dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp)
}
}
}
break
case "delete":
toRemove.add(fp)
break
}

if (argv.verbose) {
console.log(`Updated dependency graphs in ${perf.timeSince()}`)
}

// EMIT
perf.addEvent("rebuild")
let emittedFiles = 0
const destinationsToDelete = new Set<FilePath>()

for (const emitter of cfg.plugins.emitters) {
const depGraph = dependencies[emitter.name]

// emitter hasn't defined a dependency graph. call it with all processed files
if (depGraph === null) {
if (argv.verbose) {
console.log(
`Emitter ${emitter.name} doesn't define a dependency graph. Calling it with all files...`,
)
}

const files = [...contentMap.values()].filter(
([_node, vfile]) => !toRemove.has(vfile.data.filePath!),
)

const emittedFps = await emitter.emit(ctx, files, staticResources)

if (ctx.argv.verbose) {
for (const file of emittedFps) {
console.log(`[emit:${emitter.name}] ${file}`)
}
}

emittedFiles += emittedFps.length
continue
}

// only call the emitter if it uses this file
if (depGraph.hasNode(fp)) {
// re-emit using all files that are needed for the downstream of this file
// eg. for ContentIndex, the dep graph could be:
// a.md --> contentIndex.json
// b.md ------^
//
// if a.md changes, we need to re-emit contentIndex.json,
// and supply [a.md, b.md] to the emitter
const upstreams = [...depGraph.getLeafNodeAncestors(fp)] as FilePath[]

if (action === "delete" && upstreams.length === 1) {
// if there's only one upstream, the destination is solely dependent on this file
destinationsToDelete.add(upstreams[0])
}

const upstreamContent = upstreams
// filter out non-markdown files
.filter((file) => contentMap.has(file))
// if file was deleted, don't give it to the emitter
.filter((file) => !toRemove.has(file))
.map((file) => contentMap.get(file)!)

const emittedFps = await emitter.emit(ctx, upstreamContent, staticResources)

if (ctx.argv.verbose) {
for (const file of emittedFps) {
console.log(`[emit:${emitter.name}] ${file}`)
}
}

emittedFiles += emittedFps.length
}
}

console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`)

// CLEANUP
// delete files that are solely dependent on this file
await rimraf([...destinationsToDelete])
for (const file of toRemove) {
// remove from cache
contentMap.delete(file)
// remove the node from dependency graphs
Object.values(dependencies).forEach((depGraph) => depGraph?.removeNode(file))
}

toRemove.clear()
release()
clientRefresh()
}

async function rebuildFromEntrypoint(
fp: string,
action: "add" | "change" | "delete",
action: FileEvent,
clientRefresh: () => void,
buildData: BuildData, // note: this function mutates buildData
) {
Expand Down
5 changes: 5 additions & 0 deletions quartz/cli/args.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ export const BuildArgv = {
default: false,
describe: "run a local server to live-preview your Quartz",
},
fastRebuild: {
boolean: true,
default: false,
describe: "[experimental] rebuild only the changed files",
},
baseDir: {
string: true,
default: "",
Expand Down
8 changes: 6 additions & 2 deletions quartz/components/Head.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ export default (() => {
<link rel="icon" href={iconPath} />
<meta name="description" content={description} />
<meta name="generator" content="Quartz" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
{cfg.theme.cdnCaching && (
<>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
</>
)}
{css.map((href) => (
<link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve />
))}
Expand Down
7 changes: 5 additions & 2 deletions quartz/components/renderPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ export function renderPage(
type: "element",
tagName: "a",
properties: { href: inner.properties?.href, class: ["internal"] },
children: [{ type: "text", value: `Link to original` }],
children: [
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
],
},
]
}
Expand Down Expand Up @@ -207,8 +209,9 @@ export function renderPage(
</div>
)

const lang = componentData.frontmatter?.lang ?? cfg.locale?.split("-")[0] ?? "en"
const doc = (
<html>
<html lang={lang}>
<Head {...componentData} />
<body data-slug={slug}>
<div id="quartz-root" class="page">
Expand Down
4 changes: 4 additions & 0 deletions quartz/components/styles/search.scss
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@
margin: 0 auto;
width: min($pageWidth, 100%);
}

a[role="anchor"] {
background-color: transparent;
}
}

& > #results-container {
Expand Down
Loading

0 comments on commit 823cde5

Please sign in to comment.