fix(fast rebuild): handle added an deleted markdown correctly (#921)
* Handle added files correctly * Handle deletes properly * addGraph renamed to mergeGraph
This commit is contained in:
parent
6be1ed1ea2
commit
a6417c447a
@ -185,9 +185,14 @@ async function partialRebuildFromEntrypoint(
|
||||
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 @@ async function partialRebuildFromEntrypoint(
|
||||
// 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 @@ async function partialRebuildFromEntrypoint(
|
||||
// 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 @@ async function partialRebuildFromEntrypoint(
|
||||
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()
|
||||
|
@ -39,6 +39,28 @@ describe("DepGraph", () => {
|
||||
})
|
||||
})
|
||||
|
||||
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
|
||||
|
@ -39,12 +39,26 @@ export default class DepGraph<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 @@ export default class DepGraph<T> {
|
||||
|
||||
// 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 @@ export default class DepGraph<T> {
|
||||
})
|
||||
}
|
||||
|
||||
// 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 ---^
|
||||
|
Loading…
Reference in New Issue
Block a user