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:
		@@ -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)
 | 
				
			||||||
    // remove the node from dependency graphs
 | 
					    Object.values(dependencies).forEach((depGraph) => {
 | 
				
			||||||
    Object.values(dependencies).forEach((depGraph) => depGraph?.removeNode(file))
 | 
					      // remove the node from dependency graphs
 | 
				
			||||||
 | 
					      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 ---^
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user