| quartz/build.ts | ●●●●● patch | view | raw | blame | history | |
| quartz/depgraph.test.ts | ●●●●● patch | view | raw | blame | history | |
| quartz/depgraph.ts | ●●●●● patch | view | raw | blame | history |
quartz/build.ts
@@ -185,9 +185,14 @@ 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) const existingGraph = dependencies[emitter.name] if (existingGraph !== null) { existingGraph.mergeGraph(emitterGraph) } else { // might be the first time we're adding a mardown file dependencies[emitter.name] = emitterGraph } } } break @@ -224,7 +229,6 @@ // EMIT perf.addEvent("rebuild") let emittedFiles = 0 const destinationsToDelete = new Set<FilePath>() for (const emitter of cfg.plugins.emitters) { const depGraph = dependencies[emitter.name] @@ -264,11 +268,6 @@ // 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)) @@ -291,14 +290,24 @@ 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]) const destinationsToDelete = new Set<FilePath>() for (const file of toRemove) { // remove from cache contentMap.delete(file) Object.values(dependencies).forEach((depGraph) => { // remove the node from dependency graphs Object.values(dependencies).forEach((depGraph) => depGraph?.removeNode(file)) depGraph?.removeNode(file) // remove any orphan nodes. eg if a.md is deleted, a.html is orphaned and should be removed const orphanNodes = depGraph?.removeOrphanNodes() orphanNodes?.forEach((node) => { // only delete files that are in the output directory if (node.startsWith(argv.output)) { destinationsToDelete.add(node) } }) }) } await rimraf([...destinationsToDelete]) toRemove.clear() release() quartz/depgraph.test.ts
@@ -39,6 +39,28 @@ }) }) describe("mergeGraph", () => { test("merges two graphs", () => { const graph = new DepGraph<string>() graph.addEdge("A.md", "A.html") const other = new DepGraph<string>() other.addEdge("B.md", "B.html") graph.mergeGraph(other) const expected = { nodes: ["A.md", "A.html", "B.md", "B.html"], edges: [ ["A.md", "A.html"], ["B.md", "B.html"], ], } assert.deepStrictEqual(graph.export(), expected) }) }) describe("updateIncomingEdgesForNode", () => { test("merges when node exists", () => { // A.md -> B.md -> B.html quartz/depgraph.ts
@@ -39,12 +39,26 @@ } } // Remove node and all edges connected to it removeNode(node: T): void { if (this._graph.has(node)) { // first remove all edges so other nodes don't have references to this node for (const target of this._graph.get(node)!.outgoing) { this.removeEdge(node, target) } for (const source of this._graph.get(node)!.incoming) { this.removeEdge(source, node) } this._graph.delete(node) } } forEachNode(callback: (node: T) => void): void { for (const node of this._graph.keys()) { callback(node) } } hasEdge(from: T, to: T): boolean { return Boolean(this._graph.get(from)?.outgoing.has(to)) } @@ -92,6 +106,15 @@ // DEPENDENCY ALGORITHMS // Add all nodes and edges from other graph to this graph mergeGraph(other: DepGraph<T>): void { other.forEachEdge(([source, target]) => { this.addNode(source) this.addNode(target) this.addEdge(source, target) }) } // 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 @@ -112,6 +135,24 @@ }) } // Remove all nodes that do not have any incoming or outgoing edges // A node may be orphaned if the only node pointing to it was removed removeOrphanNodes(): Set<T> { let orphanNodes = new Set<T>() this.forEachNode((node) => { if (this.inDegree(node) === 0 && this.outDegree(node) === 0) { orphanNodes.add(node) } }) orphanNodes.forEach((node) => { this.removeNode(node) }) return orphanNodes } // Get all leaf nodes (i.e. destination paths) reachable from the node provided // Eg. if the graph is A -> B -> C // D ---^