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 =
|
const emitterGraph =
|
||||||
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
|
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
|
||||||
|
|
||||||
// emmiter may not define a dependency graph. nothing to update if so
|
|
||||||
if (emitterGraph) {
|
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
|
break
|
||||||
@ -224,7 +229,6 @@ async function partialRebuildFromEntrypoint(
|
|||||||
// EMIT
|
// EMIT
|
||||||
perf.addEvent("rebuild")
|
perf.addEvent("rebuild")
|
||||||
let emittedFiles = 0
|
let emittedFiles = 0
|
||||||
const destinationsToDelete = new Set<FilePath>()
|
|
||||||
|
|
||||||
for (const emitter of cfg.plugins.emitters) {
|
for (const emitter of cfg.plugins.emitters) {
|
||||||
const depGraph = dependencies[emitter.name]
|
const depGraph = dependencies[emitter.name]
|
||||||
@ -264,11 +268,6 @@ async function partialRebuildFromEntrypoint(
|
|||||||
// and supply [a.md, b.md] to the emitter
|
// and supply [a.md, b.md] to the emitter
|
||||||
const upstreams = [...depGraph.getLeafNodeAncestors(fp)] as FilePath[]
|
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
|
const upstreamContent = upstreams
|
||||||
// filter out non-markdown files
|
// filter out non-markdown files
|
||||||
.filter((file) => contentMap.has(file))
|
.filter((file) => contentMap.has(file))
|
||||||
@ -291,14 +290,24 @@ async function partialRebuildFromEntrypoint(
|
|||||||
console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`)
|
console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`)
|
||||||
|
|
||||||
// CLEANUP
|
// CLEANUP
|
||||||
// delete files that are solely dependent on this file
|
const destinationsToDelete = new Set<FilePath>()
|
||||||
await rimraf([...destinationsToDelete])
|
|
||||||
for (const file of toRemove) {
|
for (const file of toRemove) {
|
||||||
// remove from cache
|
// remove from cache
|
||||||
contentMap.delete(file)
|
contentMap.delete(file)
|
||||||
|
Object.values(dependencies).forEach((depGraph) => {
|
||||||
// remove the node from dependency graphs
|
// 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()
|
toRemove.clear()
|
||||||
release()
|
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", () => {
|
describe("updateIncomingEdgesForNode", () => {
|
||||||
test("merges when node exists", () => {
|
test("merges when node exists", () => {
|
||||||
// A.md -> B.md -> B.html
|
// 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 {
|
removeNode(node: T): void {
|
||||||
if (this._graph.has(node)) {
|
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)
|
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 {
|
hasEdge(from: T, to: T): boolean {
|
||||||
return Boolean(this._graph.get(from)?.outgoing.has(to))
|
return Boolean(this._graph.get(from)?.outgoing.has(to))
|
||||||
}
|
}
|
||||||
@ -92,6 +106,15 @@ export default class DepGraph<T> {
|
|||||||
|
|
||||||
// DEPENDENCY ALGORITHMS
|
// 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:
|
// For the node provided:
|
||||||
// If node does not exist, add it
|
// 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 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
|
// Get all leaf nodes (i.e. destination paths) reachable from the node provided
|
||||||
// Eg. if the graph is A -> B -> C
|
// Eg. if the graph is A -> B -> C
|
||||||
// D ---^
|
// D ---^
|
||||||
|
Loading…
Reference in New Issue
Block a user