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