feat(experimental): partial rebuilds (#716)
This commit is contained in:
parent
a87704cd05
commit
fe353d946b
@ -15,7 +15,7 @@
|
|||||||
"docs": "npx quartz build --serve -d docs",
|
"docs": "npx quartz build --serve -d docs",
|
||||||
"check": "tsc --noEmit && npx prettier . --check",
|
"check": "tsc --noEmit && npx prettier . --check",
|
||||||
"format": "npx prettier . --write",
|
"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"
|
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
194
quartz/build.ts
194
quartz/build.ts
@ -17,6 +17,10 @@ import { glob, toPosixPath } from "./util/glob"
|
|||||||
import { trace } from "./util/trace"
|
import { trace } from "./util/trace"
|
||||||
import { options } from "./util/sourcemap"
|
import { options } from "./util/sourcemap"
|
||||||
import { Mutex } from "async-mutex"
|
import { Mutex } from "async-mutex"
|
||||||
|
import DepGraph from "./depgraph"
|
||||||
|
import { getStaticResourcesFromPlugins } from "./plugins"
|
||||||
|
|
||||||
|
type Dependencies = Record<string, DepGraph<FilePath> | null>
|
||||||
|
|
||||||
type BuildData = {
|
type BuildData = {
|
||||||
ctx: BuildCtx
|
ctx: BuildCtx
|
||||||
@ -29,8 +33,11 @@ type BuildData = {
|
|||||||
toRebuild: Set<FilePath>
|
toRebuild: Set<FilePath>
|
||||||
toRemove: Set<FilePath>
|
toRemove: Set<FilePath>
|
||||||
lastBuildMs: number
|
lastBuildMs: number
|
||||||
|
dependencies: Dependencies
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FileEvent = "add" | "change" | "delete"
|
||||||
|
|
||||||
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||||
const ctx: BuildCtx = {
|
const ctx: BuildCtx = {
|
||||||
argv,
|
argv,
|
||||||
@ -68,12 +75,24 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
|||||||
|
|
||||||
const parsedFiles = await parseMarkdown(ctx, filePaths)
|
const parsedFiles = await parseMarkdown(ctx, filePaths)
|
||||||
const filteredContent = filterContent(ctx, parsedFiles)
|
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)
|
await emitContent(ctx, filteredContent)
|
||||||
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
|
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
|
||||||
release()
|
release()
|
||||||
|
|
||||||
if (argv.serve) {
|
if (argv.serve) {
|
||||||
return startServing(ctx, mut, parsedFiles, clientRefresh)
|
return startServing(ctx, mut, parsedFiles, clientRefresh, dependencies)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,9 +102,11 @@ async function startServing(
|
|||||||
mut: Mutex,
|
mut: Mutex,
|
||||||
initialContent: ProcessedContent[],
|
initialContent: ProcessedContent[],
|
||||||
clientRefresh: () => void,
|
clientRefresh: () => void,
|
||||||
|
dependencies: Dependencies, // emitter name: dep graph
|
||||||
) {
|
) {
|
||||||
const { argv } = ctx
|
const { argv } = ctx
|
||||||
|
|
||||||
|
// cache file parse results
|
||||||
const contentMap = new Map<FilePath, ProcessedContent>()
|
const contentMap = new Map<FilePath, ProcessedContent>()
|
||||||
for (const content of initialContent) {
|
for (const content of initialContent) {
|
||||||
const [_tree, vfile] = content
|
const [_tree, vfile] = content
|
||||||
@ -95,6 +116,7 @@ async function startServing(
|
|||||||
const buildData: BuildData = {
|
const buildData: BuildData = {
|
||||||
ctx,
|
ctx,
|
||||||
mut,
|
mut,
|
||||||
|
dependencies,
|
||||||
contentMap,
|
contentMap,
|
||||||
ignored: await isGitIgnored(),
|
ignored: await isGitIgnored(),
|
||||||
initialSlugs: ctx.allSlugs,
|
initialSlugs: ctx.allSlugs,
|
||||||
@ -110,19 +132,181 @@ async function startServing(
|
|||||||
ignoreInitial: true,
|
ignoreInitial: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const buildFromEntry = argv.fastRebuild ? partialRebuildFromEntrypoint : rebuildFromEntrypoint
|
||||||
watcher
|
watcher
|
||||||
.on("add", (fp) => rebuildFromEntrypoint(fp, "add", clientRefresh, buildData))
|
.on("add", (fp) => buildFromEntry(fp, "add", clientRefresh, buildData))
|
||||||
.on("change", (fp) => rebuildFromEntrypoint(fp, "change", clientRefresh, buildData))
|
.on("change", (fp) => buildFromEntry(fp, "change", clientRefresh, buildData))
|
||||||
.on("unlink", (fp) => rebuildFromEntrypoint(fp, "delete", clientRefresh, buildData))
|
.on("unlink", (fp) => buildFromEntry(fp, "delete", clientRefresh, buildData))
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
await watcher.close()
|
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(
|
async function rebuildFromEntrypoint(
|
||||||
fp: string,
|
fp: string,
|
||||||
action: "add" | "change" | "delete",
|
action: FileEvent,
|
||||||
clientRefresh: () => void,
|
clientRefresh: () => void,
|
||||||
buildData: BuildData, // note: this function mutates buildData
|
buildData: BuildData, // note: this function mutates buildData
|
||||||
) {
|
) {
|
||||||
|
@ -71,6 +71,11 @@ export const BuildArgv = {
|
|||||||
default: false,
|
default: false,
|
||||||
describe: "run a local server to live-preview your Quartz",
|
describe: "run a local server to live-preview your Quartz",
|
||||||
},
|
},
|
||||||
|
fastRebuild: {
|
||||||
|
boolean: true,
|
||||||
|
default: false,
|
||||||
|
describe: "[experimental] rebuild only the changed files",
|
||||||
|
},
|
||||||
baseDir: {
|
baseDir: {
|
||||||
string: true,
|
string: true,
|
||||||
default: "",
|
default: "",
|
||||||
|
96
quartz/depgraph.test.ts
Normal file
96
quartz/depgraph.test.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import test, { describe } from "node:test"
|
||||||
|
import DepGraph from "./depgraph"
|
||||||
|
import assert from "node:assert"
|
||||||
|
|
||||||
|
describe("DepGraph", () => {
|
||||||
|
test("getLeafNodes", () => {
|
||||||
|
const graph = new DepGraph<string>()
|
||||||
|
graph.addEdge("A", "B")
|
||||||
|
graph.addEdge("B", "C")
|
||||||
|
graph.addEdge("D", "C")
|
||||||
|
assert.deepStrictEqual(graph.getLeafNodes("A"), new Set(["C"]))
|
||||||
|
assert.deepStrictEqual(graph.getLeafNodes("B"), new Set(["C"]))
|
||||||
|
assert.deepStrictEqual(graph.getLeafNodes("C"), new Set(["C"]))
|
||||||
|
assert.deepStrictEqual(graph.getLeafNodes("D"), new Set(["C"]))
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getLeafNodeAncestors", () => {
|
||||||
|
test("gets correct ancestors in a graph without cycles", () => {
|
||||||
|
const graph = new DepGraph<string>()
|
||||||
|
graph.addEdge("A", "B")
|
||||||
|
graph.addEdge("B", "C")
|
||||||
|
graph.addEdge("D", "B")
|
||||||
|
assert.deepStrictEqual(graph.getLeafNodeAncestors("A"), new Set(["A", "B", "D"]))
|
||||||
|
assert.deepStrictEqual(graph.getLeafNodeAncestors("B"), new Set(["A", "B", "D"]))
|
||||||
|
assert.deepStrictEqual(graph.getLeafNodeAncestors("C"), new Set(["A", "B", "D"]))
|
||||||
|
assert.deepStrictEqual(graph.getLeafNodeAncestors("D"), new Set(["A", "B", "D"]))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("gets correct ancestors in a graph with cycles", () => {
|
||||||
|
const graph = new DepGraph<string>()
|
||||||
|
graph.addEdge("A", "B")
|
||||||
|
graph.addEdge("B", "C")
|
||||||
|
graph.addEdge("C", "A")
|
||||||
|
graph.addEdge("C", "D")
|
||||||
|
assert.deepStrictEqual(graph.getLeafNodeAncestors("A"), new Set(["A", "B", "C"]))
|
||||||
|
assert.deepStrictEqual(graph.getLeafNodeAncestors("B"), new Set(["A", "B", "C"]))
|
||||||
|
assert.deepStrictEqual(graph.getLeafNodeAncestors("C"), new Set(["A", "B", "C"]))
|
||||||
|
assert.deepStrictEqual(graph.getLeafNodeAncestors("D"), new Set(["A", "B", "C"]))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("updateIncomingEdgesForNode", () => {
|
||||||
|
test("merges when node exists", () => {
|
||||||
|
// A.md -> B.md -> B.html
|
||||||
|
const graph = new DepGraph<string>()
|
||||||
|
graph.addEdge("A.md", "B.md")
|
||||||
|
graph.addEdge("B.md", "B.html")
|
||||||
|
|
||||||
|
// B.md is edited so it removes the A.md transclusion
|
||||||
|
// and adds C.md transclusion
|
||||||
|
// C.md -> B.md
|
||||||
|
const other = new DepGraph<string>()
|
||||||
|
other.addEdge("C.md", "B.md")
|
||||||
|
other.addEdge("B.md", "B.html")
|
||||||
|
|
||||||
|
// A.md -> B.md removed, C.md -> B.md added
|
||||||
|
// C.md -> B.md -> B.html
|
||||||
|
graph.updateIncomingEdgesForNode(other, "B.md")
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
nodes: ["A.md", "B.md", "B.html", "C.md"],
|
||||||
|
edges: [
|
||||||
|
["B.md", "B.html"],
|
||||||
|
["C.md", "B.md"],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.deepStrictEqual(graph.export(), expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("adds node if it does not exist", () => {
|
||||||
|
// A.md -> B.md
|
||||||
|
const graph = new DepGraph<string>()
|
||||||
|
graph.addEdge("A.md", "B.md")
|
||||||
|
|
||||||
|
// Add a new file C.md that transcludes B.md
|
||||||
|
// B.md -> C.md
|
||||||
|
const other = new DepGraph<string>()
|
||||||
|
other.addEdge("B.md", "C.md")
|
||||||
|
|
||||||
|
// B.md -> C.md added
|
||||||
|
// A.md -> B.md -> C.md
|
||||||
|
graph.updateIncomingEdgesForNode(other, "C.md")
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
nodes: ["A.md", "B.md", "C.md"],
|
||||||
|
edges: [
|
||||||
|
["A.md", "B.md"],
|
||||||
|
["B.md", "C.md"],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.deepStrictEqual(graph.export(), expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
187
quartz/depgraph.ts
Normal file
187
quartz/depgraph.ts
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
export default class DepGraph<T> {
|
||||||
|
// node: incoming and outgoing edges
|
||||||
|
_graph = new Map<T, { incoming: Set<T>; outgoing: Set<T> }>()
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._graph = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
export(): Object {
|
||||||
|
return {
|
||||||
|
nodes: this.nodes,
|
||||||
|
edges: this.edges,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return JSON.stringify(this.export(), null, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BASIC GRAPH OPERATIONS
|
||||||
|
|
||||||
|
get nodes(): T[] {
|
||||||
|
return Array.from(this._graph.keys())
|
||||||
|
}
|
||||||
|
|
||||||
|
get edges(): [T, T][] {
|
||||||
|
let edges: [T, T][] = []
|
||||||
|
this.forEachEdge((edge) => edges.push(edge))
|
||||||
|
return edges
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNode(node: T): boolean {
|
||||||
|
return this._graph.has(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
addNode(node: T): void {
|
||||||
|
if (!this._graph.has(node)) {
|
||||||
|
this._graph.set(node, { incoming: new Set(), outgoing: new Set() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeNode(node: T): void {
|
||||||
|
if (this._graph.has(node)) {
|
||||||
|
this._graph.delete(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasEdge(from: T, to: T): boolean {
|
||||||
|
return Boolean(this._graph.get(from)?.outgoing.has(to))
|
||||||
|
}
|
||||||
|
|
||||||
|
addEdge(from: T, to: T): void {
|
||||||
|
this.addNode(from)
|
||||||
|
this.addNode(to)
|
||||||
|
|
||||||
|
this._graph.get(from)!.outgoing.add(to)
|
||||||
|
this._graph.get(to)!.incoming.add(from)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEdge(from: T, to: T): void {
|
||||||
|
if (this._graph.has(from) && this._graph.has(to)) {
|
||||||
|
this._graph.get(from)!.outgoing.delete(to)
|
||||||
|
this._graph.get(to)!.incoming.delete(from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns -1 if node does not exist
|
||||||
|
outDegree(node: T): number {
|
||||||
|
return this.hasNode(node) ? this._graph.get(node)!.outgoing.size : -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns -1 if node does not exist
|
||||||
|
inDegree(node: T): number {
|
||||||
|
return this.hasNode(node) ? this._graph.get(node)!.incoming.size : -1
|
||||||
|
}
|
||||||
|
|
||||||
|
forEachOutNeighbor(node: T, callback: (neighbor: T) => void): void {
|
||||||
|
this._graph.get(node)?.outgoing.forEach(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
forEachInNeighbor(node: T, callback: (neighbor: T) => void): void {
|
||||||
|
this._graph.get(node)?.incoming.forEach(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
forEachEdge(callback: (edge: [T, T]) => void): void {
|
||||||
|
for (const [source, { outgoing }] of this._graph.entries()) {
|
||||||
|
for (const target of outgoing) {
|
||||||
|
callback([source, target])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEPENDENCY ALGORITHMS
|
||||||
|
|
||||||
|
// For the node provided:
|
||||||
|
// If node does not exist, add it
|
||||||
|
// If an incoming edge was added in other, it is added in this graph
|
||||||
|
// If an incoming edge was deleted in other, it is deleted in this graph
|
||||||
|
updateIncomingEdgesForNode(other: DepGraph<T>, node: T): void {
|
||||||
|
this.addNode(node)
|
||||||
|
|
||||||
|
// Add edge if it is present in other
|
||||||
|
other.forEachInNeighbor(node, (neighbor) => {
|
||||||
|
this.addEdge(neighbor, node)
|
||||||
|
})
|
||||||
|
|
||||||
|
// For node provided, remove incoming edge if it is absent in other
|
||||||
|
this.forEachEdge(([source, target]) => {
|
||||||
|
if (target === node && !other.hasEdge(source, target)) {
|
||||||
|
this.removeEdge(source, target)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all leaf nodes (i.e. destination paths) reachable from the node provided
|
||||||
|
// Eg. if the graph is A -> B -> C
|
||||||
|
// D ---^
|
||||||
|
// and the node is B, this function returns [C]
|
||||||
|
getLeafNodes(node: T): Set<T> {
|
||||||
|
let stack: T[] = [node]
|
||||||
|
let visited = new Set<T>()
|
||||||
|
let leafNodes = new Set<T>()
|
||||||
|
|
||||||
|
// DFS
|
||||||
|
while (stack.length > 0) {
|
||||||
|
let node = stack.pop()!
|
||||||
|
|
||||||
|
// If the node is already visited, skip it
|
||||||
|
if (visited.has(node)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visited.add(node)
|
||||||
|
|
||||||
|
// Check if the node is a leaf node (i.e. destination path)
|
||||||
|
if (this.outDegree(node) === 0) {
|
||||||
|
leafNodes.add(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all unvisited neighbors to the stack
|
||||||
|
this.forEachOutNeighbor(node, (neighbor) => {
|
||||||
|
if (!visited.has(neighbor)) {
|
||||||
|
stack.push(neighbor)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return leafNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all ancestors of the leaf nodes reachable from the node provided
|
||||||
|
// Eg. if the graph is A -> B -> C
|
||||||
|
// D ---^
|
||||||
|
// and the node is B, this function returns [A, B, D]
|
||||||
|
getLeafNodeAncestors(node: T): Set<T> {
|
||||||
|
const leafNodes = this.getLeafNodes(node)
|
||||||
|
let visited = new Set<T>()
|
||||||
|
let upstreamNodes = new Set<T>()
|
||||||
|
|
||||||
|
// Backwards DFS for each leaf node
|
||||||
|
leafNodes.forEach((leafNode) => {
|
||||||
|
let stack: T[] = [leafNode]
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
let node = stack.pop()!
|
||||||
|
|
||||||
|
if (visited.has(node)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visited.add(node)
|
||||||
|
// Add node if it's not a leaf node (i.e. destination path)
|
||||||
|
// Assumes destination file cannot depend on another destination file
|
||||||
|
if (this.outDegree(node) !== 0) {
|
||||||
|
upstreamNodes.add(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all unvisited parents to the stack
|
||||||
|
this.forEachInNeighbor(node, (parentNode) => {
|
||||||
|
if (!visited.has(parentNode)) {
|
||||||
|
stack.push(parentNode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return upstreamNodes
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ import { NotFound } from "../../components"
|
|||||||
import { defaultProcessedContent } from "../vfile"
|
import { defaultProcessedContent } from "../vfile"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
|
import DepGraph from "../../depgraph"
|
||||||
|
|
||||||
export const NotFoundPage: QuartzEmitterPlugin = () => {
|
export const NotFoundPage: QuartzEmitterPlugin = () => {
|
||||||
const opts: FullPageLayout = {
|
const opts: FullPageLayout = {
|
||||||
@ -27,6 +28,9 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
|
|||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return [Head, Body, pageBody, Footer]
|
return [Head, Body, pageBody, Footer]
|
||||||
},
|
},
|
||||||
|
async getDependencyGraph(_ctx, _content, _resources) {
|
||||||
|
return new DepGraph<FilePath>()
|
||||||
|
},
|
||||||
async emit(ctx, _content, resources): Promise<FilePath[]> {
|
async emit(ctx, _content, resources): Promise<FilePath[]> {
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
const slug = "404" as FullSlug
|
const slug = "404" as FullSlug
|
||||||
|
@ -2,12 +2,17 @@ import { FilePath, FullSlug, joinSegments, resolveRelative, simplifySlug } from
|
|||||||
import { QuartzEmitterPlugin } from "../types"
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
|
import DepGraph from "../../depgraph"
|
||||||
|
|
||||||
export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
||||||
name: "AliasRedirects",
|
name: "AliasRedirects",
|
||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
|
async getDependencyGraph(_ctx, _content, _resources) {
|
||||||
|
// TODO implement
|
||||||
|
return new DepGraph<FilePath>()
|
||||||
|
},
|
||||||
async emit(ctx, content, _resources): Promise<FilePath[]> {
|
async emit(ctx, content, _resources): Promise<FilePath[]> {
|
||||||
const { argv } = ctx
|
const { argv } = ctx
|
||||||
const fps: FilePath[] = []
|
const fps: FilePath[] = []
|
||||||
|
@ -3,6 +3,7 @@ import { QuartzEmitterPlugin } from "../types"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { glob } from "../../util/glob"
|
import { glob } from "../../util/glob"
|
||||||
|
import DepGraph from "../../depgraph"
|
||||||
|
|
||||||
export const Assets: QuartzEmitterPlugin = () => {
|
export const Assets: QuartzEmitterPlugin = () => {
|
||||||
return {
|
return {
|
||||||
@ -10,6 +11,24 @@ export const Assets: QuartzEmitterPlugin = () => {
|
|||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
|
async getDependencyGraph(ctx, _content, _resources) {
|
||||||
|
const { argv, cfg } = ctx
|
||||||
|
const graph = new DepGraph<FilePath>()
|
||||||
|
|
||||||
|
const fps = await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
|
||||||
|
|
||||||
|
for (const fp of fps) {
|
||||||
|
const ext = path.extname(fp)
|
||||||
|
const src = joinSegments(argv.directory, fp) as FilePath
|
||||||
|
const name = (slugifyFilePath(fp as FilePath, true) + ext) as FilePath
|
||||||
|
|
||||||
|
const dest = joinSegments(argv.output, name) as FilePath
|
||||||
|
|
||||||
|
graph.addEdge(src, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return graph
|
||||||
|
},
|
||||||
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
|
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
|
||||||
// glob all non MD/MDX/HTML files in content folder and copy it over
|
// glob all non MD/MDX/HTML files in content folder and copy it over
|
||||||
const assetsPath = argv.output
|
const assetsPath = argv.output
|
||||||
|
@ -2,6 +2,7 @@ import { FilePath, joinSegments } from "../../util/path"
|
|||||||
import { QuartzEmitterPlugin } from "../types"
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import chalk from "chalk"
|
import chalk from "chalk"
|
||||||
|
import DepGraph from "../../depgraph"
|
||||||
|
|
||||||
export function extractDomainFromBaseUrl(baseUrl: string) {
|
export function extractDomainFromBaseUrl(baseUrl: string) {
|
||||||
const url = new URL(`https://${baseUrl}`)
|
const url = new URL(`https://${baseUrl}`)
|
||||||
@ -13,6 +14,9 @@ export const CNAME: QuartzEmitterPlugin = () => ({
|
|||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
|
async getDependencyGraph(_ctx, _content, _resources) {
|
||||||
|
return new DepGraph<FilePath>()
|
||||||
|
},
|
||||||
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
|
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
|
||||||
if (!cfg.configuration.baseUrl) {
|
if (!cfg.configuration.baseUrl) {
|
||||||
console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration"))
|
console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration"))
|
||||||
|
@ -14,6 +14,7 @@ import { googleFontHref, joinStyles } from "../../util/theme"
|
|||||||
import { Features, transform } from "lightningcss"
|
import { Features, transform } from "lightningcss"
|
||||||
import { transform as transpile } from "esbuild"
|
import { transform as transpile } from "esbuild"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
|
import DepGraph from "../../depgraph"
|
||||||
|
|
||||||
type ComponentResources = {
|
type ComponentResources = {
|
||||||
css: string[]
|
css: string[]
|
||||||
@ -149,9 +150,10 @@ function addGlobalPageResources(
|
|||||||
loadTime: "afterDOMReady",
|
loadTime: "afterDOMReady",
|
||||||
contentType: "inline",
|
contentType: "inline",
|
||||||
script: `
|
script: `
|
||||||
const socket = new WebSocket('${wsUrl}')
|
const socket = new WebSocket('${wsUrl}')
|
||||||
socket.addEventListener('message', () => document.location.reload())
|
// reload(true) ensures resources like images and scripts are fetched again in firefox
|
||||||
`,
|
socket.addEventListener('message', () => document.location.reload(true))
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -171,6 +173,24 @@ export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial<
|
|||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
|
async getDependencyGraph(ctx, content, _resources) {
|
||||||
|
// This emitter adds static resources to the `resources` parameter. One
|
||||||
|
// important resource this emitter adds is the code to start a websocket
|
||||||
|
// connection and listen to rebuild messages, which triggers a page reload.
|
||||||
|
// The resources parameter with the reload logic is later used by the
|
||||||
|
// ContentPage emitter while creating the final html page. In order for
|
||||||
|
// the reload logic to be included, and so for partial rebuilds to work,
|
||||||
|
// we need to run this emitter for all markdown files.
|
||||||
|
const graph = new DepGraph<FilePath>()
|
||||||
|
|
||||||
|
for (const [_tree, file] of content) {
|
||||||
|
const sourcePath = file.data.filePath!
|
||||||
|
const slug = file.data.slug!
|
||||||
|
graph.addEdge(sourcePath, joinSegments(ctx.argv.output, slug + ".html") as FilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return graph
|
||||||
|
},
|
||||||
async emit(ctx, _content, resources): Promise<FilePath[]> {
|
async emit(ctx, _content, resources): Promise<FilePath[]> {
|
||||||
const promises: Promise<FilePath>[] = []
|
const promises: Promise<FilePath>[] = []
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
|
@ -7,6 +7,7 @@ import { QuartzEmitterPlugin } from "../types"
|
|||||||
import { toHtml } from "hast-util-to-html"
|
import { toHtml } from "hast-util-to-html"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
|
import DepGraph from "../../depgraph"
|
||||||
|
|
||||||
export type ContentIndex = Map<FullSlug, ContentDetails>
|
export type ContentIndex = Map<FullSlug, ContentDetails>
|
||||||
export type ContentDetails = {
|
export type ContentDetails = {
|
||||||
@ -92,6 +93,26 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
|
|||||||
opts = { ...defaultOptions, ...opts }
|
opts = { ...defaultOptions, ...opts }
|
||||||
return {
|
return {
|
||||||
name: "ContentIndex",
|
name: "ContentIndex",
|
||||||
|
async getDependencyGraph(ctx, content, _resources) {
|
||||||
|
const graph = new DepGraph<FilePath>()
|
||||||
|
|
||||||
|
for (const [_tree, file] of content) {
|
||||||
|
const sourcePath = file.data.filePath!
|
||||||
|
|
||||||
|
graph.addEdge(
|
||||||
|
sourcePath,
|
||||||
|
joinSegments(ctx.argv.output, "static/contentIndex.json") as FilePath,
|
||||||
|
)
|
||||||
|
if (opts?.enableSiteMap) {
|
||||||
|
graph.addEdge(sourcePath, joinSegments(ctx.argv.output, "sitemap.xml") as FilePath)
|
||||||
|
}
|
||||||
|
if (opts?.enableRSS) {
|
||||||
|
graph.addEdge(sourcePath, joinSegments(ctx.argv.output, "index.xml") as FilePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return graph
|
||||||
|
},
|
||||||
async emit(ctx, content, _resources) {
|
async emit(ctx, content, _resources) {
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
const emitted: FilePath[] = []
|
const emitted: FilePath[] = []
|
||||||
|
@ -4,11 +4,12 @@ import HeaderConstructor from "../../components/Header"
|
|||||||
import BodyConstructor from "../../components/Body"
|
import BodyConstructor from "../../components/Body"
|
||||||
import { pageResources, renderPage } from "../../components/renderPage"
|
import { pageResources, renderPage } from "../../components/renderPage"
|
||||||
import { FullPageLayout } from "../../cfg"
|
import { FullPageLayout } from "../../cfg"
|
||||||
import { FilePath, pathToRoot } from "../../util/path"
|
import { FilePath, joinSegments, pathToRoot } from "../../util/path"
|
||||||
import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
||||||
import { Content } from "../../components"
|
import { Content } from "../../components"
|
||||||
import chalk from "chalk"
|
import chalk from "chalk"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
|
import DepGraph from "../../depgraph"
|
||||||
|
|
||||||
export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
||||||
const opts: FullPageLayout = {
|
const opts: FullPageLayout = {
|
||||||
@ -27,6 +28,18 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
|
|||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
|
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
|
||||||
},
|
},
|
||||||
|
async getDependencyGraph(ctx, content, _resources) {
|
||||||
|
// TODO handle transclusions
|
||||||
|
const graph = new DepGraph<FilePath>()
|
||||||
|
|
||||||
|
for (const [_tree, file] of content) {
|
||||||
|
const sourcePath = file.data.filePath!
|
||||||
|
const slug = file.data.slug!
|
||||||
|
graph.addEdge(sourcePath, joinSegments(ctx.argv.output, slug + ".html") as FilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return graph
|
||||||
|
},
|
||||||
async emit(ctx, content, resources): Promise<FilePath[]> {
|
async emit(ctx, content, resources): Promise<FilePath[]> {
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
const fps: FilePath[] = []
|
const fps: FilePath[] = []
|
||||||
@ -60,7 +73,7 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
|
|||||||
fps.push(fp)
|
fps.push(fp)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!containsIndex) {
|
if (!containsIndex && !ctx.argv.fastRebuild) {
|
||||||
console.log(
|
console.log(
|
||||||
chalk.yellow(
|
chalk.yellow(
|
||||||
`\nWarning: you seem to be missing an \`index.md\` home page file at the root of your \`${ctx.argv.directory}\` folder. This may cause errors when deploying.`,
|
`\nWarning: you seem to be missing an \`index.md\` home page file at the root of your \`${ctx.argv.directory}\` folder. This may cause errors when deploying.`,
|
||||||
|
@ -19,6 +19,7 @@ import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.lay
|
|||||||
import { FolderContent } from "../../components"
|
import { FolderContent } from "../../components"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
|
import DepGraph from "../../depgraph"
|
||||||
|
|
||||||
export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
||||||
const opts: FullPageLayout = {
|
const opts: FullPageLayout = {
|
||||||
@ -37,6 +38,13 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpt
|
|||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
|
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
|
||||||
},
|
},
|
||||||
|
async getDependencyGraph(ctx, content, _resources) {
|
||||||
|
// Example graph:
|
||||||
|
// nested/file.md --> nested/file.html
|
||||||
|
// \-------> nested/index.html
|
||||||
|
// TODO implement
|
||||||
|
return new DepGraph<FilePath>()
|
||||||
|
},
|
||||||
async emit(ctx, content, resources): Promise<FilePath[]> {
|
async emit(ctx, content, resources): Promise<FilePath[]> {
|
||||||
const fps: FilePath[] = []
|
const fps: FilePath[] = []
|
||||||
const allFiles = content.map((c) => c[1].data)
|
const allFiles = content.map((c) => c[1].data)
|
||||||
|
@ -2,12 +2,27 @@ import { FilePath, QUARTZ, joinSegments } from "../../util/path"
|
|||||||
import { QuartzEmitterPlugin } from "../types"
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { glob } from "../../util/glob"
|
import { glob } from "../../util/glob"
|
||||||
|
import DepGraph from "../../depgraph"
|
||||||
|
|
||||||
export const Static: QuartzEmitterPlugin = () => ({
|
export const Static: QuartzEmitterPlugin = () => ({
|
||||||
name: "Static",
|
name: "Static",
|
||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
|
async getDependencyGraph({ argv, cfg }, _content, _resources) {
|
||||||
|
const graph = new DepGraph<FilePath>()
|
||||||
|
|
||||||
|
const staticPath = joinSegments(QUARTZ, "static")
|
||||||
|
const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)
|
||||||
|
for (const fp of fps) {
|
||||||
|
graph.addEdge(
|
||||||
|
joinSegments("static", fp) as FilePath,
|
||||||
|
joinSegments(argv.output, "static", fp) as FilePath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return graph
|
||||||
|
},
|
||||||
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
|
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
|
||||||
const staticPath = joinSegments(QUARTZ, "static")
|
const staticPath = joinSegments(QUARTZ, "static")
|
||||||
const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)
|
const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)
|
||||||
|
@ -16,6 +16,7 @@ import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.lay
|
|||||||
import { TagContent } from "../../components"
|
import { TagContent } from "../../components"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
|
import DepGraph from "../../depgraph"
|
||||||
|
|
||||||
export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
||||||
const opts: FullPageLayout = {
|
const opts: FullPageLayout = {
|
||||||
@ -34,6 +35,10 @@ export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts)
|
|||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
|
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
|
||||||
},
|
},
|
||||||
|
async getDependencyGraph(ctx, _content, _resources) {
|
||||||
|
// TODO implement
|
||||||
|
return new DepGraph<FilePath>()
|
||||||
|
},
|
||||||
async emit(ctx, content, resources): Promise<FilePath[]> {
|
async emit(ctx, content, resources): Promise<FilePath[]> {
|
||||||
const fps: FilePath[] = []
|
const fps: FilePath[] = []
|
||||||
const allFiles = content.map((c) => c[1].data)
|
const allFiles = content.map((c) => c[1].data)
|
||||||
|
@ -4,6 +4,7 @@ import { ProcessedContent } from "./vfile"
|
|||||||
import { QuartzComponent } from "../components/types"
|
import { QuartzComponent } from "../components/types"
|
||||||
import { FilePath } from "../util/path"
|
import { FilePath } from "../util/path"
|
||||||
import { BuildCtx } from "../util/ctx"
|
import { BuildCtx } from "../util/ctx"
|
||||||
|
import DepGraph from "../depgraph"
|
||||||
|
|
||||||
export interface PluginTypes {
|
export interface PluginTypes {
|
||||||
transformers: QuartzTransformerPluginInstance[]
|
transformers: QuartzTransformerPluginInstance[]
|
||||||
@ -38,4 +39,9 @@ export type QuartzEmitterPluginInstance = {
|
|||||||
name: string
|
name: string
|
||||||
emit(ctx: BuildCtx, content: ProcessedContent[], resources: StaticResources): Promise<FilePath[]>
|
emit(ctx: BuildCtx, content: ProcessedContent[], resources: StaticResources): Promise<FilePath[]>
|
||||||
getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
|
getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
|
||||||
|
getDependencyGraph?(
|
||||||
|
ctx: BuildCtx,
|
||||||
|
content: ProcessedContent[],
|
||||||
|
resources: StaticResources,
|
||||||
|
): Promise<DepGraph<FilePath>>
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ export interface Argv {
|
|||||||
verbose: boolean
|
verbose: boolean
|
||||||
output: string
|
output: string
|
||||||
serve: boolean
|
serve: boolean
|
||||||
|
fastRebuild: boolean
|
||||||
port: number
|
port: number
|
||||||
wsPort: number
|
wsPort: number
|
||||||
remoteDevHost?: string
|
remoteDevHost?: string
|
||||||
|
Loading…
Reference in New Issue
Block a user