about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEtomicBomb <ethan@ethan.ws>2024-07-24 22:56:38 +0000
committerEtomicBomb <ethan@ethan.ws>2024-08-07 18:37:39 +0000
commit9ebe5ae3064af4f7f0f79c774e778ee26f36bdcb (patch)
tree884a8b6cd8b93b4e80bfe2e94baae69552182274
parent9bad7ba324099d124c77c5b06aebf68e11763f7b (diff)
downloadrust-9ebe5ae3064af4f7f0f79c774e778ee26f36bdcb.tar.gz
rust-9ebe5ae3064af4f7f0f79c774e778ee26f36bdcb.zip
initial implementation of mergable rustdoc cci
-rw-r--r--src/librustdoc/Cargo.toml2
-rw-r--r--src/librustdoc/clean/types.rs2
-rw-r--r--src/librustdoc/config.rs1
-rw-r--r--src/librustdoc/html/render/context.rs10
-rw-r--r--src/librustdoc/html/render/mod.rs2
-rw-r--r--src/librustdoc/html/render/search_index.rs34
-rw-r--r--src/librustdoc/html/render/sorted_json.rs82
-rw-r--r--src/librustdoc/html/render/sorted_template.rs136
-rw-r--r--src/librustdoc/html/render/tests.rs271
-rw-r--r--src/librustdoc/html/render/write_shared.rs1462
10 files changed, 1317 insertions, 685 deletions
diff --git a/src/librustdoc/Cargo.toml b/src/librustdoc/Cargo.toml
index b3fccbf6456..67ba8c77317 100644
--- a/src/librustdoc/Cargo.toml
+++ b/src/librustdoc/Cargo.toml
@@ -16,7 +16,7 @@ minifier = "0.3.0"
 pulldown-cmark-old = { version = "0.9.6", package = "pulldown-cmark", default-features = false }
 regex = "1"
 rustdoc-json-types = { path = "../rustdoc-json-types" }
-serde_json = "1.0"
+serde_json = { version = "1.0", features = ["preserve_order"] }
 serde = { version = "1.0", features = ["derive"] }
 smallvec = "1.8.1"
 tempfile = "3"
diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs
index 4850500a1bf..542e810b5cf 100644
--- a/src/librustdoc/clean/types.rs
+++ b/src/librustdoc/clean/types.rs
@@ -128,7 +128,7 @@ pub(crate) struct ExternalCrate {
 }
 
 impl ExternalCrate {
-    const LOCAL: Self = Self { crate_num: LOCAL_CRATE };
+    pub(crate) const LOCAL: Self = Self { crate_num: LOCAL_CRATE };
 
     #[inline]
     pub(crate) fn def_id(&self) -> DefId {
diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs
index e4549796b3e..2e54a22840b 100644
--- a/src/librustdoc/config.rs
+++ b/src/librustdoc/config.rs
@@ -730,7 +730,6 @@ impl Options {
         let extern_html_root_takes_precedence =
             matches.opt_present("extern-html-root-takes-precedence");
         let html_no_source = matches.opt_present("html-no-source");
-
         if generate_link_to_definition && (show_coverage || output_format != OutputFormat::Html) {
             dcx.fatal(
                 "--generate-link-to-definition option can only be used with HTML output format",
diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs
index 0334eacc161..8e72dd6a864 100644
--- a/src/librustdoc/html/render/context.rs
+++ b/src/librustdoc/html/render/context.rs
@@ -14,7 +14,6 @@ use rustc_span::edition::Edition;
 use rustc_span::{sym, FileName, Symbol};
 
 use super::print_item::{full_path, item_path, print_item};
-use super::search_index::build_index;
 use super::sidebar::{print_sidebar, sidebar_module_like, Sidebar};
 use super::write_shared::write_shared;
 use super::{collect_spans_and_sources, scrape_examples_help, AllTypes, LinkFromSrc, StylePath};
@@ -573,13 +572,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
         }
 
         if !no_emit_shared {
-            // Build our search index
-            let index = build_index(&krate, &mut Rc::get_mut(&mut cx.shared).unwrap().cache, tcx);
-
-            // Write shared runs within a flock; disable thread dispatching of IO temporarily.
-            Rc::get_mut(&mut cx.shared).unwrap().fs.set_sync_only(true);
-            write_shared(&mut cx, &krate, index, &md_opts)?;
-            Rc::get_mut(&mut cx.shared).unwrap().fs.set_sync_only(false);
+            write_shared(&mut cx, &krate, &md_opts, tcx)?;
         }
 
         Ok((cx, krate))
@@ -729,6 +722,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
         );
         shared.fs.write(help_file, v)?;
 
+        // if to avoid writing files to doc root unless we're on the final invocation
         if shared.layout.scrape_examples_extension {
             page.title = "About scraped examples";
             page.description = "How the scraped examples feature works in Rustdoc";
diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs
index 9074e40a536..4b1c9b4af47 100644
--- a/src/librustdoc/html/render/mod.rs
+++ b/src/librustdoc/html/render/mod.rs
@@ -31,6 +31,8 @@ mod tests;
 mod context;
 mod print_item;
 pub(crate) mod sidebar;
+mod sorted_json;
+mod sorted_template;
 mod span_map;
 mod type_layout;
 mod write_shared;
diff --git a/src/librustdoc/html/render/search_index.rs b/src/librustdoc/html/render/search_index.rs
index 8a2f31f7413..184e5afba3c 100644
--- a/src/librustdoc/html/render/search_index.rs
+++ b/src/librustdoc/html/render/search_index.rs
@@ -18,6 +18,7 @@ use crate::formats::cache::{Cache, OrphanImplItem};
 use crate::formats::item_type::ItemType;
 use crate::html::format::join_with_double_colon;
 use crate::html::markdown::short_markdown_summary;
+use crate::html::render::sorted_json::SortedJson;
 use crate::html::render::{self, IndexItem, IndexItemFunctionType, RenderType, RenderTypeId};
 
 /// The serialized search description sharded version
@@ -46,7 +47,7 @@ use crate::html::render::{self, IndexItem, IndexItemFunctionType, RenderType, Re
 /// [2]: https://en.wikipedia.org/wiki/Sliding_window_protocol#Basic_concept
 /// [3]: https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/description-tcp-features
 pub(crate) struct SerializedSearchIndex {
-    pub(crate) index: String,
+    pub(crate) index: SortedJson,
     pub(crate) desc: Vec<(usize, String)>,
 }
 
@@ -683,24 +684,19 @@ pub(crate) fn build_index<'tcx>(
     // The index, which is actually used to search, is JSON
     // It uses `JSON.parse(..)` to actually load, since JSON
     // parses faster than the full JavaScript syntax.
-    let index = format!(
-        r#"["{}",{}]"#,
-        krate.name(tcx),
-        serde_json::to_string(&CrateData {
-            items: crate_items,
-            paths: crate_paths,
-            aliases: &aliases,
-            associated_item_disambiguators: &associated_item_disambiguators,
-            desc_index,
-            empty_desc,
-        })
-        .expect("failed serde conversion")
-        // All these `replace` calls are because we have to go through JS string for JSON content.
-        .replace('\\', r"\\")
-        .replace('\'', r"\'")
-        // We need to escape double quotes for the JSON.
-        .replace("\\\"", "\\\\\"")
-    );
+    let crate_name = krate.name(tcx);
+    let data = CrateData {
+        items: crate_items,
+        paths: crate_paths,
+        aliases: &aliases,
+        associated_item_disambiguators: &associated_item_disambiguators,
+        desc_index,
+        empty_desc,
+    };
+    let index = SortedJson::array_unsorted([
+        SortedJson::serialize(crate_name.as_str()),
+        SortedJson::serialize(data),
+    ]);
     SerializedSearchIndex { index, desc }
 }
 
diff --git a/src/librustdoc/html/render/sorted_json.rs b/src/librustdoc/html/render/sorted_json.rs
new file mode 100644
index 00000000000..3a097733b8b
--- /dev/null
+++ b/src/librustdoc/html/render/sorted_json.rs
@@ -0,0 +1,82 @@
+use itertools::Itertools as _;
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use std::borrow::Borrow;
+use std::fmt;
+
+/// Prerenedered json.
+///
+/// Arrays are sorted by their stringified entries, and objects are sorted by their stringified
+/// keys.
+///
+/// Must use serde_json with the preserve_order feature.
+///
+/// Both the Display and serde_json::to_string implementations write the serialized json
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+#[serde(from = "Value")]
+#[serde(into = "Value")]
+pub(crate) struct SortedJson(String);
+
+impl SortedJson {
+    /// If you pass in an array, it will not be sorted.
+    pub(crate) fn serialize<T: Serialize>(item: T) -> Self {
+        SortedJson(serde_json::to_string(&item).unwrap())
+    }
+
+    /// Serializes and sorts
+    pub(crate) fn array<T: Borrow<SortedJson>, I: IntoIterator<Item = T>>(items: I) -> Self {
+        let items = items
+            .into_iter()
+            .sorted_unstable_by(|a, b| a.borrow().cmp(&b.borrow()))
+            .format_with(",", |item, f| f(item.borrow()));
+        SortedJson(format!("[{}]", items))
+    }
+
+    pub(crate) fn array_unsorted<T: Borrow<SortedJson>, I: IntoIterator<Item = T>>(
+        items: I,
+    ) -> Self {
+        let items = items.into_iter().format_with(",", |item, f| f(item.borrow()));
+        SortedJson(format!("[{items}]"))
+    }
+}
+
+impl fmt::Display for SortedJson {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+impl From<Value> for SortedJson {
+    fn from(value: Value) -> Self {
+        SortedJson(serde_json::to_string(&value).unwrap())
+    }
+}
+
+impl From<SortedJson> for Value {
+    fn from(json: SortedJson) -> Self {
+        serde_json::from_str(&json.0).unwrap()
+    }
+}
+
+/// For use in JSON.parse('{...}').
+///
+/// JSON.parse supposedly loads faster than raw JS source,
+/// so this is used for large objects.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub(crate) struct EscapedJson(SortedJson);
+
+impl From<SortedJson> for EscapedJson {
+    fn from(json: SortedJson) -> Self {
+        EscapedJson(json)
+    }
+}
+
+impl fmt::Display for EscapedJson {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        // All these `replace` calls are because we have to go through JS string
+        // for JSON content.
+        // We need to escape double quotes for the JSON
+        let json = self.0.0.replace('\\', r"\\").replace('\'', r"\'").replace("\\\"", "\\\\\"");
+        write!(f, "{}", json)
+    }
+}
diff --git a/src/librustdoc/html/render/sorted_template.rs b/src/librustdoc/html/render/sorted_template.rs
new file mode 100644
index 00000000000..95240616b01
--- /dev/null
+++ b/src/librustdoc/html/render/sorted_template.rs
@@ -0,0 +1,136 @@
+use std::collections::BTreeSet;
+use std::fmt;
+use std::marker::PhantomData;
+use std::str::FromStr;
+
+use serde::{Deserialize, Serialize};
+
+/// Append-only templates for sorted, deduplicated lists of items.
+///
+/// Last line of the rendered output is a comment encoding the next insertion point.
+#[derive(Debug, Clone)]
+pub(crate) struct SortedTemplate<F> {
+    format: PhantomData<F>,
+    before: String,
+    after: String,
+    contents: BTreeSet<String>,
+}
+
+/// Written to last line of file to specify the location of each fragment
+#[derive(Serialize, Deserialize, Debug, Clone)]
+struct Offset {
+    /// Index of the first byte in the template
+    start: usize,
+    /// The length of each fragment in the encoded template, including the separator
+    delta: Vec<usize>,
+}
+
+impl<F> SortedTemplate<F> {
+    /// Generate this template from arbitary text.
+    /// Will insert wherever the substring `magic` can be found.
+    /// Errors if it does not appear exactly once.
+    pub(crate) fn magic(template: &str, magic: &str) -> Result<Self, Error> {
+        let mut split = template.split(magic);
+        let before = split.next().ok_or(Error)?;
+        let after = split.next().ok_or(Error)?;
+        if split.next().is_some() {
+            return Err(Error);
+        }
+        Ok(Self::before_after(before, after))
+    }
+
+    /// Template will insert contents between `before` and `after`
+    pub(crate) fn before_after<S: ToString, T: ToString>(before: S, after: T) -> Self {
+        let before = before.to_string();
+        let after = after.to_string();
+        SortedTemplate { format: PhantomData, before, after, contents: Default::default() }
+    }
+}
+
+impl<F: FileFormat> SortedTemplate<F> {
+    /// Adds this text to the template
+    pub(crate) fn append(&mut self, insert: String) {
+        self.contents.insert(insert);
+    }
+}
+
+impl<F: FileFormat> fmt::Display for SortedTemplate<F> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let mut delta = Vec::default();
+        write!(f, "{}", self.before)?;
+        let contents: Vec<_> = self.contents.iter().collect();
+        let mut sep = "";
+        for content in contents {
+            delta.push(sep.len() + content.len());
+            write!(f, "{}{}", sep, content)?;
+            sep = F::SEPARATOR;
+        }
+        let offset = Offset { start: self.before.len(), delta };
+        let offset = serde_json::to_string(&offset).unwrap();
+        write!(f, "{}\n{}{}{}", self.after, F::COMMENT_START, offset, F::COMMENT_END)?;
+        Ok(())
+    }
+}
+
+fn checked_split_at(s: &str, index: usize) -> Option<(&str, &str)> {
+    s.is_char_boundary(index).then(|| s.split_at(index))
+}
+
+impl<F: FileFormat> FromStr for SortedTemplate<F> {
+    type Err = Error;
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let (s, offset) = s.rsplit_once("\n").ok_or(Error)?;
+        let offset = offset.strip_prefix(F::COMMENT_START).ok_or(Error)?;
+        let offset = offset.strip_suffix(F::COMMENT_END).ok_or(Error)?;
+        let offset: Offset = serde_json::from_str(&offset).map_err(|_| Error)?;
+        let (before, mut s) = checked_split_at(s, offset.start).ok_or(Error)?;
+        let mut contents = BTreeSet::default();
+        let mut sep = "";
+        for &index in offset.delta.iter() {
+            let (content, rest) = checked_split_at(s, index).ok_or(Error)?;
+            s = rest;
+            let content = content.strip_prefix(sep).ok_or(Error)?;
+            contents.insert(content.to_string());
+            sep = F::SEPARATOR;
+        }
+        Ok(SortedTemplate {
+            format: PhantomData,
+            before: before.to_string(),
+            after: s.to_string(),
+            contents,
+        })
+    }
+}
+
+pub(crate) trait FileFormat {
+    const COMMENT_START: &'static str;
+    const COMMENT_END: &'static str;
+    const SEPARATOR: &'static str;
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct Html;
+
+impl FileFormat for Html {
+    const COMMENT_START: &'static str = "<!--";
+    const COMMENT_END: &'static str = "-->";
+    const SEPARATOR: &'static str = "";
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct Js;
+
+impl FileFormat for Js {
+    const COMMENT_START: &'static str = "//";
+    const COMMENT_END: &'static str = "";
+    const SEPARATOR: &'static str = ",";
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct Error;
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "invalid template")
+    }
+}
diff --git a/src/librustdoc/html/render/tests.rs b/src/librustdoc/html/render/tests.rs
index 4a9724a6f84..16e67b0f118 100644
--- a/src/librustdoc/html/render/tests.rs
+++ b/src/librustdoc/html/render/tests.rs
@@ -52,3 +52,274 @@ fn test_all_types_prints_header_once() {
 
     assert_eq!(1, buffer.into_inner().matches("List of all items").count());
 }
+
+mod sorted_json {
+    use super::super::sorted_json::*;
+
+    fn check(json: SortedJson, serialized: &str) {
+        assert_eq!(json.to_string(), serialized);
+        assert_eq!(serde_json::to_string(&json).unwrap(), serialized);
+
+        let json = json.to_string();
+        let json: SortedJson = serde_json::from_str(&json).unwrap();
+
+        assert_eq!(json.to_string(), serialized);
+        assert_eq!(serde_json::to_string(&json).unwrap(), serialized);
+
+        let json = serde_json::to_string(&json).unwrap();
+        let json: SortedJson = serde_json::from_str(&json).unwrap();
+
+        assert_eq!(json.to_string(), serialized);
+        assert_eq!(serde_json::to_string(&json).unwrap(), serialized);
+    }
+
+    #[test]
+    fn escape_json_number() {
+        let json = SortedJson::serialize(3);
+        let json = EscapedJson::from(json);
+        assert_eq!(format!("{json}"), "3");
+    }
+
+    #[test]
+    fn escape_json_single_quote() {
+        let json = SortedJson::serialize("he's");
+        let json = EscapedJson::from(json);
+        assert_eq!(format!("{json}"), r#""he\'s""#);
+    }
+
+    #[test]
+    fn escape_json_array() {
+        let json = SortedJson::serialize([1, 2, 3]);
+        let json = EscapedJson::from(json);
+        assert_eq!(format!("{json}"), r#"[1,2,3]"#);
+    }
+
+    #[test]
+    fn escape_json_string() {
+        let json = SortedJson::serialize(r#"he"llo"#);
+        let json = EscapedJson::from(json);
+        assert_eq!(format!("{json}"), r#""he\\\"llo""#);
+    }
+
+    #[test]
+    fn escape_json_string_escaped() {
+        let json = SortedJson::serialize(r#"he\"llo"#);
+        let json = EscapedJson::from(json);
+        assert_eq!(format!("{json}"), r#""he\\\\\\\"llo""#);
+    }
+
+    #[test]
+    fn escape_json_string_escaped_escaped() {
+        let json = SortedJson::serialize(r#"he\\"llo"#);
+        let json = EscapedJson::from(json);
+        assert_eq!(format!("{json}"), r#""he\\\\\\\\\\\"llo""#);
+    }
+
+    #[test]
+    fn number() {
+        let json = SortedJson::serialize(3);
+        let serialized = "3";
+        check(json, serialized);
+    }
+
+    #[test]
+    fn boolean() {
+        let json = SortedJson::serialize(true);
+        let serialized = "true";
+        check(json, serialized);
+    }
+
+    #[test]
+    fn string() {
+        let json = SortedJson::serialize("he\"llo");
+        let serialized = r#""he\"llo""#;
+        check(json, serialized);
+    }
+
+    #[test]
+    fn serialize_array() {
+        let json = SortedJson::serialize([3, 1, 2]);
+        let serialized = "[3,1,2]";
+        check(json, serialized);
+    }
+
+    #[test]
+    fn sorted_array() {
+        let items = ["c", "a", "b"];
+        let serialized = r#"["a","b","c"]"#;
+        let items: Vec<SortedJson> = items.into_iter().map(SortedJson::serialize).collect();
+        let json = SortedJson::array(items);
+        check(json, serialized);
+    }
+
+    #[test]
+    fn nested_array() {
+        let a = SortedJson::serialize(3);
+        let b = SortedJson::serialize(2);
+        let c = SortedJson::serialize(1);
+        let d = SortedJson::serialize([1, 3, 2]);
+        let json = SortedJson::array([a, b, c, d]);
+        let serialized = r#"[1,2,3,[1,3,2]]"#;
+        check(json, serialized);
+    }
+
+    #[test]
+    fn array_unsorted() {
+        let items = ["c", "a", "b"];
+        let serialized = r#"["c","a","b"]"#;
+        let items: Vec<SortedJson> = items.into_iter().map(SortedJson::serialize).collect();
+        let json = SortedJson::array_unsorted(items);
+        check(json, serialized);
+    }
+}
+
+mod sorted_template {
+    use super::super::sorted_template::*;
+    use std::str::FromStr;
+
+    fn is_comment_js(s: &str) -> bool {
+        s.starts_with("//")
+    }
+
+    fn is_comment_html(s: &str) -> bool {
+        // not correct but good enough for these tests
+        s.starts_with("<!--") && s.ends_with("-->")
+    }
+
+    #[test]
+    fn html_from_empty() {
+        let inserts = ["<p>hello</p>", "<p>kind</p>", "<p>hello</p>", "<p>world</p>"];
+        let mut template = SortedTemplate::<Html>::before_after("", "");
+        for insert in inserts {
+            template.append(insert.to_string());
+        }
+        let template = format!("{template}");
+        let (template, end) = template.rsplit_once("\n").unwrap();
+        assert_eq!(template, "<p>hello</p><p>kind</p><p>world</p>");
+        assert!(is_comment_html(end));
+        assert!(!end.contains("\n"));
+    }
+
+    #[test]
+    fn html_page() {
+        let inserts = ["<p>hello</p>", "<p>kind</p>", "<p>world</p>"];
+        let before = "<html><head></head><body>";
+        let after = "</body>";
+        let mut template = SortedTemplate::<Html>::before_after(before, after);
+        for insert in inserts {
+            template.append(insert.to_string());
+        }
+        let template = format!("{template}");
+        let (template, end) = template.rsplit_once("\n").unwrap();
+        assert_eq!(template, format!("{before}{}{after}", inserts.join("")));
+        assert!(is_comment_html(end));
+        assert!(!end.contains("\n"));
+    }
+
+    #[test]
+    fn js_from_empty() {
+        let inserts = ["1", "2", "2", "2", "3", "1"];
+        let mut template = SortedTemplate::<Js>::before_after("", "");
+        for insert in inserts {
+            template.append(insert.to_string());
+        }
+        let template = format!("{template}");
+        let (template, end) = template.rsplit_once("\n").unwrap();
+        assert_eq!(template, "1,2,3");
+        assert!(is_comment_js(end));
+        assert!(!end.contains("\n"));
+    }
+
+    #[test]
+    fn js_empty_array() {
+        let template = SortedTemplate::<Js>::before_after("[", "]");
+        let template = format!("{template}");
+        let (template, end) = template.rsplit_once("\n").unwrap();
+        assert_eq!(template, format!("[]"));
+        assert!(is_comment_js(end));
+        assert!(!end.contains("\n"));
+    }
+
+    #[test]
+    fn js_number_array() {
+        let inserts = ["1", "2", "3"];
+        let mut template = SortedTemplate::<Js>::before_after("[", "]");
+        for insert in inserts {
+            template.append(insert.to_string());
+        }
+        let template = format!("{template}");
+        let (template, end) = template.rsplit_once("\n").unwrap();
+        assert_eq!(template, format!("[1,2,3]"));
+        assert!(is_comment_js(end));
+        assert!(!end.contains("\n"));
+    }
+
+    #[test]
+    fn magic_js_number_array() {
+        let inserts = ["1", "1"];
+        let mut template = SortedTemplate::<Js>::magic("[#]", "#").unwrap();
+        for insert in inserts {
+            template.append(insert.to_string());
+        }
+        let template = format!("{template}");
+        let (template, end) = template.rsplit_once("\n").unwrap();
+        assert_eq!(template, format!("[1]"));
+        assert!(is_comment_js(end));
+        assert!(!end.contains("\n"));
+    }
+
+    #[test]
+    fn round_trip_js() {
+        let inserts = ["1", "2", "3"];
+        let mut template = SortedTemplate::<Js>::before_after("[", "]");
+        for insert in inserts {
+            template.append(insert.to_string());
+        }
+        let template1 = format!("{template}");
+        let mut template = SortedTemplate::<Js>::from_str(&template1).unwrap();
+        assert_eq!(template1, format!("{template}"));
+        template.append("4".to_string());
+        let template = format!("{template}");
+        let (template, end) = template.rsplit_once("\n").unwrap();
+        assert_eq!(template, "[1,2,3,4]");
+        assert!(is_comment_js(end));
+    }
+
+    #[test]
+    fn round_trip_html() {
+        let inserts = ["<p>hello</p>", "<p>kind</p>", "<p>world</p>", "<p>kind</p>"];
+        let before = "<html><head></head><body>";
+        let after = "</body>";
+        let mut template = SortedTemplate::<Html>::before_after(before, after);
+        template.append(inserts[0].to_string());
+        template.append(inserts[1].to_string());
+        let template = format!("{template}");
+        let mut template = SortedTemplate::<Html>::from_str(&template).unwrap();
+        template.append(inserts[2].to_string());
+        let template = format!("{template}");
+        let (template, end) = template.rsplit_once("\n").unwrap();
+        assert_eq!(template, format!("{before}<p>hello</p><p>kind</p><p>world</p>{after}"));
+        assert!(is_comment_html(end));
+    }
+
+    #[test]
+    fn blank_js() {
+        let inserts = ["1", "2", "3"];
+        let template = SortedTemplate::<Js>::before_after("", "");
+        let template = format!("{template}");
+        let (t, _) = template.rsplit_once("\n").unwrap();
+        assert_eq!(t, "");
+        let mut template = SortedTemplate::<Js>::from_str(&template).unwrap();
+        for insert in inserts {
+            template.append(insert.to_string());
+        }
+        let template1 = format!("{template}");
+        let mut template = SortedTemplate::<Js>::from_str(&template1).unwrap();
+        assert_eq!(template1, format!("{template}"));
+        template.append("4".to_string());
+        let template = format!("{template}");
+        let (template, end) = template.rsplit_once("\n").unwrap();
+        assert_eq!(template, "1,2,3,4");
+        assert!(is_comment_js(end));
+    }
+}
diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs
index 8fd56eae37f..eaebeadd881 100644
--- a/src/librustdoc/html/render/write_shared.rs
+++ b/src/librustdoc/html/render/write_shared.rs
@@ -1,19 +1,43 @@
+//! Rustdoc writes out two kinds of shared files:
+//!  - Static files, which are embedded in the rustdoc binary and are written with a
+//!    filename that includes a hash of their contents. These will always have a new
+//!    URL if the contents change, so they are safe to cache with the
+//!    `Cache-Control: immutable` directive. They are written under the static.files/
+//!    directory and are written when --emit-type is empty (default) or contains
+//!    "toolchain-specific". If using the --static-root-path flag, it should point
+//!    to a URL path prefix where each of these filenames can be fetched.
+//!  - Invocation specific files. These are generated based on the crate(s) being
+//!    documented. Their filenames need to be predictable without knowing their
+//!    contents, so they do not include a hash in their filename and are not safe to
+//!    cache with `Cache-Control: immutable`. They include the contents of the
+//!    --resource-suffix flag and are emitted when --emit-type is empty (default)
+//!    or contains "invocation-specific".
+
+use std::any::Any;
 use std::cell::RefCell;
-use std::fs::{self, File};
-use std::io::prelude::*;
-use std::io::{self, BufReader};
-use std::path::{Component, Path};
+use std::collections::hash_map::Entry;
+use std::ffi::OsString;
+use std::fs::File;
+use std::io::BufWriter;
+use std::io::Write as _;
+use std::iter::once;
+use std::marker::PhantomData;
+use std::path::{Component, Path, PathBuf};
 use std::rc::{Rc, Weak};
+use std::str::FromStr;
+use std::{fmt, fs, io};
 
 use indexmap::IndexMap;
 use itertools::Itertools;
+use regex::Regex;
 use rustc_data_structures::flock;
 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
 use rustc_middle::ty::fast_reject::{DeepRejectCtxt, TreatParams};
+use rustc_middle::ty::TyCtxt;
 use rustc_span::def_id::DefId;
 use rustc_span::Symbol;
 use serde::ser::SerializeSeq;
-use serde::{Serialize, Serializer};
+use serde::{de::DeserializeOwned, Deserialize, Serialize, Serializer};
 
 use super::{collect_paths_for_type, ensure_trailing_slash, Context, RenderMode};
 use crate::clean::{Crate, Item, ItemId, ItemKind};
@@ -24,53 +48,92 @@ use crate::formats::cache::Cache;
 use crate::formats::item_type::ItemType;
 use crate::formats::Impl;
 use crate::html::format::Buffer;
+use crate::html::layout;
+use crate::html::render::search_index::build_index;
 use crate::html::render::search_index::SerializedSearchIndex;
+use crate::html::render::sorted_json::{EscapedJson, SortedJson};
+use crate::html::render::sorted_template::{self, SortedTemplate};
 use crate::html::render::{AssocItemLink, ImplRenderingParameters};
-use crate::html::{layout, static_files};
+use crate::html::static_files::{self, suffix_path};
 use crate::visit::DocVisitor;
 use crate::{try_err, try_none};
 
-/// Rustdoc writes out two kinds of shared files:
-///  - Static files, which are embedded in the rustdoc binary and are written with a
-///    filename that includes a hash of their contents. These will always have a new
-///    URL if the contents change, so they are safe to cache with the
-///    `Cache-Control: immutable` directive. They are written under the static.files/
-///    directory and are written when --emit-type is empty (default) or contains
-///    "toolchain-specific". If using the --static-root-path flag, it should point
-///    to a URL path prefix where each of these filenames can be fetched.
-///  - Invocation specific files. These are generated based on the crate(s) being
-///    documented. Their filenames need to be predictable without knowing their
-///    contents, so they do not include a hash in their filename and are not safe to
-///    cache with `Cache-Control: immutable`. They include the contents of the
-///    --resource-suffix flag and are emitted when --emit-type is empty (default)
-///    or contains "invocation-specific".
-pub(super) fn write_shared(
+/// Write crate-info.json cross-crate information, static files, invocation-specific files, etc. to disk
+pub(crate) fn write_shared(
     cx: &mut Context<'_>,
     krate: &Crate,
-    search_index: SerializedSearchIndex,
-    options: &RenderOptions,
+    opt: &RenderOptions,
+    tcx: TyCtxt<'_>,
 ) -> Result<(), Error> {
-    // Write out the shared files. Note that these are shared among all rustdoc
-    // docs placed in the output directory, so this needs to be a synchronized
-    // operation with respect to all other rustdocs running around.
+    // NOTE(EtomicBomb): I don't think we need sync here because no read-after-write?
+    Rc::get_mut(&mut cx.shared).unwrap().fs.set_sync_only(true);
     let lock_file = cx.dst.join(".lock");
+    // Write shared runs within a flock; disable thread dispatching of IO temporarily.
     let _lock = try_err!(flock::Lock::new(&lock_file, true, true, true), &lock_file);
 
-    // InvocationSpecific resources should always be dynamic.
-    let write_invocation_specific = |p: &str, make_content: &dyn Fn() -> Result<Vec<u8>, Error>| {
-        let content = make_content()?;
-        if options.emit.is_empty() || options.emit.contains(&EmitType::InvocationSpecific) {
-            let output_filename = static_files::suffix_path(p, &cx.shared.resource_suffix);
-            cx.shared.fs.write(cx.dst.join(output_filename), content)
-        } else {
-            Ok(())
-        }
+    let SerializedSearchIndex { index, desc } =
+        build_index(&krate, &mut Rc::get_mut(&mut cx.shared).unwrap().cache, tcx);
+    write_search_desc(cx, &krate, &desc)?; // does not need to be merged; written unconditionally
+
+    let crate_name = krate.name(cx.tcx());
+    let crate_name = crate_name.as_str(); // rand
+    let crate_name_json = SortedJson::serialize(crate_name); // "rand"
+    let external_crates = hack_get_external_crate_names(cx)?;
+    let info = CrateInfo {
+        src_files_js: SourcesPart::get(cx, &crate_name_json)?,
+        search_index_js: SearchIndexPart::get(cx, index)?,
+        all_crates: AllCratesPart::get(crate_name_json.clone())?,
+        crates_index: CratesIndexPart::get(&crate_name, &external_crates)?,
+        trait_impl: TraitAliasPart::get(cx, &crate_name_json)?,
+        type_impl: TypeAliasPart::get(cx, krate, &crate_name_json)?,
     };
 
-    cx.shared
-        .fs
-        .create_dir_all(cx.dst.join("static.files"))
-        .map_err(|e| PathError::new(e, "static.files"))?;
+    let crates_info = vec![info]; // we have info from just one crate
+
+    write_static_files(cx, &opt)?;
+    let dst = &cx.dst;
+    if opt.emit.is_empty() || opt.emit.contains(&EmitType::InvocationSpecific) {
+        if cx.include_sources {
+            write_rendered_cci::<SourcesPart, _>(SourcesPart::blank, dst, &crates_info)?;
+        }
+        write_rendered_cci::<SearchIndexPart, _>(
+            SearchIndexPart::blank,
+            dst,
+            &crates_info,
+        )?;
+        write_rendered_cci::<AllCratesPart, _>(AllCratesPart::blank, dst, &crates_info)?;
+    }
+    write_rendered_cci::<TraitAliasPart, _>(TraitAliasPart::blank, dst, &crates_info)?;
+    write_rendered_cci::<TypeAliasPart, _>(TypeAliasPart::blank, dst, &crates_info)?;
+    match &opt.index_page {
+        Some(index_page) if opt.enable_index_page => {
+            let mut md_opts = opt.clone();
+            md_opts.output = cx.dst.clone();
+            md_opts.external_html = cx.shared.layout.external_html.clone();
+            try_err!(
+                crate::markdown::render(&index_page, md_opts, cx.shared.edition()),
+                &index_page
+            );
+        }
+        None if opt.enable_index_page => {
+            write_rendered_cci::<CratesIndexPart, _>(
+                || CratesIndexPart::blank(cx),
+                dst,
+                &crates_info,
+            )?;
+        }
+        _ => {} // they don't want an index page
+    }
+
+    Rc::get_mut(&mut cx.shared).unwrap().fs.set_sync_only(false);
+    Ok(())
+}
+
+/// Writes the static files, the style files, and the css extensions
+fn write_static_files(cx: &mut Context<'_>, options: &RenderOptions) -> Result<(), Error> {
+    let static_dir = cx.dst.join("static.files");
+
+    cx.shared.fs.create_dir_all(&static_dir).map_err(|e| PathError::new(e, "static.files"))?;
 
     // Handle added third-party themes
     for entry in &cx.shared.style_files {
@@ -97,680 +160,769 @@ pub(super) fn write_shared(
     }
 
     if options.emit.is_empty() || options.emit.contains(&EmitType::Toolchain) {
-        let static_dir = cx.dst.join(Path::new("static.files"));
         static_files::for_each(|f: &static_files::StaticFile| {
             let filename = static_dir.join(f.output_filename());
             cx.shared.fs.write(filename, f.minified())
         })?;
     }
 
-    /// Read a file and return all lines that match the `"{crate}":{data},` format,
-    /// and return a tuple `(Vec<DataString>, Vec<CrateNameString>)`.
-    ///
-    /// This forms the payload of files that look like this:
-    ///
-    /// ```javascript
-    /// var data = {
-    /// "{crate1}":{data},
-    /// "{crate2}":{data}
-    /// };
-    /// use_data(data);
-    /// ```
-    ///
-    /// The file needs to be formatted so that *only crate data lines start with `"`*.
-    fn collect(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
-        let mut ret = Vec::new();
-        let mut krates = Vec::new();
-
-        if path.exists() {
-            let prefix = format!("\"{krate}\"");
-            for line in BufReader::new(File::open(path)?).lines() {
-                let line = line?;
-                if !line.starts_with('"') {
-                    continue;
-                }
-                if line.starts_with(&prefix) {
-                    continue;
-                }
-                if line.ends_with(',') {
-                    ret.push(line[..line.len() - 1].to_string());
-                } else {
-                    // No comma (it's the case for the last added crate line)
-                    ret.push(line.to_string());
-                }
-                krates.push(
-                    line.split('"')
-                        .find(|s| !s.is_empty())
-                        .map(|s| s.to_owned())
-                        .unwrap_or_else(String::new),
-                );
-            }
-        }
-        Ok((ret, krates))
-    }
-
-    /// Read a file and return all lines that match the <code>"{crate}":{data},\ </code> format,
-    /// and return a tuple `(Vec<DataString>, Vec<CrateNameString>)`.
-    ///
-    /// This forms the payload of files that look like this:
-    ///
-    /// ```javascript
-    /// var data = JSON.parse('{\
-    /// "{crate1}":{data},\
-    /// "{crate2}":{data}\
-    /// }');
-    /// use_data(data);
-    /// ```
-    ///
-    /// The file needs to be formatted so that *only crate data lines start with `"`*.
-    fn collect_json(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
-        let mut ret = Vec::new();
-        let mut krates = Vec::new();
-
-        if path.exists() {
-            let prefix = format!("[\"{krate}\"");
-            for line in BufReader::new(File::open(path)?).lines() {
-                let line = line?;
-                if !line.starts_with("[\"") {
-                    continue;
-                }
-                if line.starts_with(&prefix) {
-                    continue;
-                }
-                if line.ends_with("],\\") {
-                    ret.push(line[..line.len() - 2].to_string());
-                } else {
-                    // Ends with "\\" (it's the case for the last added crate line)
-                    ret.push(line[..line.len() - 1].to_string());
-                }
-                krates.push(
-                    line[1..] // We skip the `[` parent at the beginning of the line.
-                        .split('"')
-                        .find(|s| !s.is_empty())
-                        .map(|s| s.to_owned())
-                        .unwrap_or_else(String::new),
-                );
-            }
-        }
-        Ok((ret, krates))
+    Ok(())
+}
+
+/// Write the search description shards to disk
+fn write_search_desc(
+    cx: &mut Context<'_>,
+    krate: &Crate,
+    search_desc: &[(usize, String)],
+) -> Result<(), Error> {
+    let crate_name = krate.name(cx.tcx()).to_string();
+    let encoded_crate_name = SortedJson::serialize(&crate_name);
+    let path = PathBuf::from_iter([&cx.dst, Path::new("search.desc"), Path::new(&crate_name)]);
+    if Path::new(&path).exists() {
+        try_err!(fs::remove_dir_all(&path), &path);
+    }
+    for (i, (_, part)) in search_desc.iter().enumerate() {
+        let filename = static_files::suffix_path(
+            &format!("{crate_name}-desc-{i}-.js"),
+            &cx.shared.resource_suffix,
+        );
+        let path = path.join(filename);
+        let part = SortedJson::serialize(&part);
+        let part = format!("searchState.loadedDescShard({encoded_crate_name}, {i}, {part})");
+        write_create_parents(&path, part)?;
     }
+    Ok(())
+}
 
-    use std::ffi::OsString;
+/// Written to `crate-info.json`. Contains pre-rendered contents to insert into the CCI template
+#[derive(Serialize, Deserialize, Clone, Debug)]
+struct CrateInfo {
+    src_files_js: PartsAndLocations<SourcesPart>,
+    search_index_js: PartsAndLocations<SearchIndexPart>,
+    all_crates: PartsAndLocations<AllCratesPart>,
+    crates_index: PartsAndLocations<CratesIndexPart>,
+    trait_impl: PartsAndLocations<TraitAliasPart>,
+    type_impl: PartsAndLocations<TypeAliasPart>,
+}
 
-    #[derive(Debug, Default)]
-    struct Hierarchy {
-        parent: Weak<Self>,
-        elem: OsString,
-        children: RefCell<FxHashMap<OsString, Rc<Self>>>,
-        elems: RefCell<FxHashSet<OsString>>,
+impl CrateInfo {
+    /// Gets a reference to the cross-crate information parts for `T`
+    fn get<T: CciPart>(&self) -> &PartsAndLocations<T> {
+        (&self.src_files_js as &dyn Any)
+            .downcast_ref()
+            .or_else(|| (&self.search_index_js as &dyn Any).downcast_ref())
+            .or_else(|| (&self.all_crates as &dyn Any).downcast_ref())
+            .or_else(|| (&self.crates_index as &dyn Any).downcast_ref())
+            .or_else(|| (&self.trait_impl as &dyn Any).downcast_ref())
+            .or_else(|| (&self.type_impl as &dyn Any).downcast_ref())
+            .expect("this should be an exhaustive list of `CciPart`s")
     }
+}
 
-    impl Hierarchy {
-        fn with_parent(elem: OsString, parent: &Rc<Self>) -> Self {
-            Self { elem, parent: Rc::downgrade(parent), ..Self::default() }
-        }
+/// Paths (relative to the doc root) and their pre-merge contents
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(transparent)]
+struct PartsAndLocations<P> {
+    parts: Vec<(PathBuf, P)>,
+}
 
-        fn to_json_string(&self) -> String {
-            let borrow = self.children.borrow();
-            let mut subs: Vec<_> = borrow.values().collect();
-            subs.sort_unstable_by(|a, b| a.elem.cmp(&b.elem));
-            let mut files = self
-                .elems
-                .borrow()
-                .iter()
-                .map(|s| format!("\"{}\"", s.to_str().expect("invalid osstring conversion")))
-                .collect::<Vec<_>>();
-            files.sort_unstable();
-            let subs = subs.iter().map(|s| s.to_json_string()).collect::<Vec<_>>().join(",");
-            let dirs = if subs.is_empty() && files.is_empty() {
-                String::new()
-            } else {
-                format!(",[{subs}]")
-            };
-            let files = files.join(",");
-            let files = if files.is_empty() { String::new() } else { format!(",[{files}]") };
-            format!(
-                "[\"{name}\"{dirs}{files}]",
-                name = self.elem.to_str().expect("invalid osstring conversion"),
-                dirs = dirs,
-                files = files
-            )
-        }
+impl<P> Default for PartsAndLocations<P> {
+    fn default() -> Self {
+        Self { parts: Vec::default() }
+    }
+}
 
-        fn add_path(self: &Rc<Self>, path: &Path) {
-            let mut h = Rc::clone(&self);
-            let mut elems = path
-                .components()
-                .filter_map(|s| match s {
-                    Component::Normal(s) => Some(s.to_owned()),
-                    Component::ParentDir => Some(OsString::from("..")),
-                    _ => None,
-                })
-                .peekable();
-            loop {
-                let cur_elem = elems.next().expect("empty file path");
-                if cur_elem == ".." {
-                    if let Some(parent) = h.parent.upgrade() {
-                        h = parent;
-                    }
-                    continue;
-                }
-                if elems.peek().is_none() {
-                    h.elems.borrow_mut().insert(cur_elem);
-                    break;
-                } else {
-                    let entry = Rc::clone(
-                        h.children
-                            .borrow_mut()
-                            .entry(cur_elem.clone())
-                            .or_insert_with(|| Rc::new(Self::with_parent(cur_elem, &h))),
-                    );
-                    h = entry;
-                }
-            }
+impl<T, U> PartsAndLocations<Part<T, U>> {
+    fn push(&mut self, path: PathBuf, item: U) {
+        self.parts.push((path, Part { _artifact: PhantomData, item }));
+    }
+
+    /// Singleton part, one file
+    fn with(path: PathBuf, part: U) -> Self {
+        let mut ret = Self::default();
+        ret.push(path, part);
+        ret
+    }
+}
+
+/// A piece of one of the shared artifacts for documentation (search index, sources, alias list, etc.)
+///
+/// Merged at a user specified time and written to the `doc/` directory
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(transparent)]
+struct Part<T, U> {
+    #[serde(skip)]
+    _artifact: PhantomData<T>,
+    item: U,
+}
+
+impl<T, U: fmt::Display> fmt::Display for Part<T, U> {
+    /// Writes serialized JSON
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.item)
+    }
+}
+
+/// Wrapper trait for `Part<T, U>`
+trait CciPart: Sized + fmt::Display + DeserializeOwned + 'static {
+    /// Identifies the file format of the cross-crate information
+    type FileFormat: sorted_template::FileFormat;
+}
+
+#[derive(Serialize, Deserialize, Clone, Default, Debug)]
+struct SearchIndex;
+type SearchIndexPart = Part<SearchIndex, EscapedJson>;
+impl CciPart for SearchIndexPart {
+    type FileFormat = sorted_template::Js;
+}
+
+impl SearchIndexPart {
+    fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
+        SortedTemplate::before_after(
+            r"var searchIndex = new Map(JSON.parse('[",
+            r"]'));
+if (typeof exports !== 'undefined') exports.searchIndex = searchIndex;
+else if (window.initSearch) window.initSearch(searchIndex);",
+        )
+    }
+
+    fn get(cx: &Context<'_>, search_index: SortedJson) -> Result<PartsAndLocations<Self>, Error> {
+        let path = suffix_path("search-index.js", &cx.shared.resource_suffix);
+        let search_index = EscapedJson::from(search_index);
+        Ok(PartsAndLocations::with(path, search_index))
+    }
+}
+
+#[derive(Serialize, Deserialize, Clone, Default, Debug)]
+struct AllCrates;
+type AllCratesPart = Part<AllCrates, SortedJson>;
+impl CciPart for AllCratesPart {
+    type FileFormat = sorted_template::Js;
+}
+
+impl AllCratesPart {
+    fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
+        SortedTemplate::before_after("window.ALL_CRATES = [", "];")
+    }
+
+    fn get(crate_name_json: SortedJson) -> Result<PartsAndLocations<Self>, Error> {
+        // external hack_get_external_crate_names not needed here, because
+        // there's no way that we write the search index but not crates.js
+        let path = PathBuf::from("crates.js");
+        Ok(PartsAndLocations::with(path, crate_name_json))
+    }
+}
+
+/// Reads `crates.js`, which seems like the best
+/// place to obtain the list of externally documented crates if the index
+/// page was disabled when documenting the deps.
+///
+/// This is to match the current behavior of rustdoc, which allows you to get all crates
+/// on the index page, even if --enable-index-page is only passed to the last crate.
+fn hack_get_external_crate_names(cx: &Context<'_>) -> Result<Vec<String>, Error> {
+    let path = cx.dst.join("crates.js");
+    let Ok(content) = fs::read_to_string(&path) else {
+        // they didn't emit invocation specific, so we just say there were no crates
+        return Ok(Vec::default());
+    };
+    // this is only run once so it's fine not to cache it
+    // !dot_matches_new_line: all crates on same line. greedy: match last bracket
+    let regex = Regex::new(r"\[.*\]").unwrap();
+    let Some(content) = regex.find(&content) else {
+        return Err(Error::new("could not find crates list in crates.js", path));
+    };
+    let content: Vec<String> = try_err!(serde_json::from_str(content.as_str()), &path);
+    Ok(content)
+}
+
+#[derive(Serialize, Deserialize, Clone, Default, Debug)]
+struct CratesIndex;
+type CratesIndexPart = Part<CratesIndex, String>;
+impl CciPart for CratesIndexPart {
+    type FileFormat = sorted_template::Html;
+}
+
+impl CratesIndexPart {
+    fn blank(cx: &Context<'_>) -> SortedTemplate<<Self as CciPart>::FileFormat> {
+        let page = layout::Page {
+            title: "Index of crates",
+            css_class: "mod sys",
+            root_path: "./",
+            static_root_path: cx.shared.static_root_path.as_deref(),
+            description: "List of crates",
+            resource_suffix: &cx.shared.resource_suffix,
+            rust_logo: true,
+        };
+        let layout = &cx.shared.layout;
+        let style_files = &cx.shared.style_files;
+        const MAGIC: &str = "\u{FFFC}"; // users are being naughty if they have this
+        let content = format!("<h1>List of all crates</h1><ul class=\"all-items\">{MAGIC}</ul>");
+        let template = layout::render(layout, &page, "", content, &style_files);
+        match SortedTemplate::magic(&template, MAGIC) {
+            Ok(template) => template,
+            Err(e) => panic!(
+                "{e}: Object Replacement Character (U+FFFC) should not appear in the --index-page"
+            ),
         }
     }
 
-    if cx.include_sources {
-        let hierarchy = Rc::new(Hierarchy::default());
-        for source in cx
-            .shared
-            .local_sources
-            .iter()
-            .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
-        {
-            hierarchy.add_path(source);
+    /// Might return parts that are duplicate with ones in prexisting index.html
+    fn get(crate_name: &str, external_crates: &[String]) -> Result<PartsAndLocations<Self>, Error> {
+        let mut ret = PartsAndLocations::default();
+        let path = PathBuf::from("index.html");
+        for crate_name in external_crates.iter().map(|s| s.as_str()).chain(once(crate_name)) {
+            let part = format!(
+                "<li><a href=\"{trailing_slash}index.html\">{crate_name}</a></li>",
+                trailing_slash = ensure_trailing_slash(crate_name),
+            );
+            ret.push(path.clone(), part);
         }
-        let hierarchy = Rc::try_unwrap(hierarchy).unwrap();
-        let dst = cx.dst.join(&format!("src-files{}.js", cx.shared.resource_suffix));
-        let make_sources = || {
-            let (mut all_sources, _krates) =
-                try_err!(collect_json(&dst, krate.name(cx.tcx()).as_str()), &dst);
-            all_sources.push(format!(
-                r#"["{}",{}]"#,
-                &krate.name(cx.tcx()),
-                hierarchy
-                    .to_json_string()
-                    // All these `replace` calls are because we have to go through JS string for JSON content.
-                    .replace('\\', r"\\")
-                    .replace('\'', r"\'")
-                    // We need to escape double quotes for the JSON.
-                    .replace("\\\"", "\\\\\"")
-            ));
-            all_sources.sort();
-            // This needs to be `var`, not `const`.
-            // This variable needs declared in the current global scope so that if
-            // src-script.js loads first, it can pick it up.
-            let mut v = String::from("var srcIndex = new Map(JSON.parse('[\\\n");
-            v.push_str(&all_sources.join(",\\\n"));
-            v.push_str("\\\n]'));\ncreateSrcSidebar();\n");
-            Ok(v.into_bytes())
-        };
-        write_invocation_specific("src-files.js", &make_sources)?;
+        Ok(ret)
     }
+}
 
-    // Update the search index and crate list.
-    let dst = cx.dst.join(&format!("search-index{}.js", cx.shared.resource_suffix));
-    let (mut all_indexes, mut krates) =
-        try_err!(collect_json(&dst, krate.name(cx.tcx()).as_str()), &dst);
-    all_indexes.push(search_index.index);
-    krates.push(krate.name(cx.tcx()).to_string());
-    krates.sort();
+#[derive(Serialize, Deserialize, Clone, Default, Debug)]
+struct Sources;
+type SourcesPart = Part<Sources, EscapedJson>;
+impl CciPart for SourcesPart {
+    type FileFormat = sorted_template::Js;
+}
 
-    // Sort the indexes by crate so the file will be generated identically even
-    // with rustdoc running in parallel.
-    all_indexes.sort();
-    write_invocation_specific("search-index.js", &|| {
+impl SourcesPart {
+    fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
         // This needs to be `var`, not `const`.
         // This variable needs declared in the current global scope so that if
-        // search.js loads first, it can pick it up.
-        let mut v = String::from("var searchIndex = new Map(JSON.parse('[\\\n");
-        v.push_str(&all_indexes.join(",\\\n"));
-        v.push_str(
-            r#"\
-]'));
-if (typeof exports !== 'undefined') exports.searchIndex = searchIndex;
-else if (window.initSearch) window.initSearch(searchIndex);
-"#,
-        );
-        Ok(v.into_bytes())
-    })?;
-
-    let search_desc_dir = cx.dst.join(format!("search.desc/{krate}", krate = krate.name(cx.tcx())));
-    if Path::new(&search_desc_dir).exists() {
-        try_err!(std::fs::remove_dir_all(&search_desc_dir), &search_desc_dir);
-    }
-    try_err!(std::fs::create_dir_all(&search_desc_dir), &search_desc_dir);
-    let kratename = krate.name(cx.tcx()).to_string();
-    for (i, (_, data)) in search_index.desc.into_iter().enumerate() {
-        let output_filename = static_files::suffix_path(
-            &format!("{kratename}-desc-{i}-.js"),
-            &cx.shared.resource_suffix,
-        );
-        let path = search_desc_dir.join(output_filename);
-        try_err!(
-            std::fs::write(
-                &path,
-                &format!(
-                    r##"searchState.loadedDescShard({kratename}, {i}, {data})"##,
-                    kratename = serde_json::to_string(&kratename).unwrap(),
-                    data = serde_json::to_string(&data).unwrap(),
-                )
-                .into_bytes()
-            ),
-            &path
-        );
+        // src-script.js loads first, it can pick it up.
+        SortedTemplate::before_after(
+            r"var srcIndex = new Map(JSON.parse('[",
+            r"]'));
+createSrcSidebar();",
+        )
     }
 
-    write_invocation_specific("crates.js", &|| {
-        let krates = krates.iter().map(|k| format!("\"{k}\"")).join(",");
-        Ok(format!("window.ALL_CRATES = [{krates}];").into_bytes())
-    })?;
+    fn get(cx: &Context<'_>, crate_name: &SortedJson) -> Result<PartsAndLocations<Self>, Error> {
+        let hierarchy = Rc::new(Hierarchy::default());
+        cx.shared
+            .local_sources
+            .iter()
+            .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
+            .for_each(|source| hierarchy.add_path(source));
+        let path = suffix_path("src-files.js", &cx.shared.resource_suffix);
+        let hierarchy = hierarchy.to_json_string();
+        let part = SortedJson::array_unsorted([crate_name, &hierarchy]);
+        let part = EscapedJson::from(part);
+        Ok(PartsAndLocations::with(path, part))
+    }
+}
 
-    if options.enable_index_page {
-        if let Some(index_page) = options.index_page.clone() {
-            let mut md_opts = options.clone();
-            md_opts.output = cx.dst.clone();
-            md_opts.external_html = (*cx.shared).layout.external_html.clone();
+/// Source files directory tree
+#[derive(Debug, Default)]
+struct Hierarchy {
+    parent: Weak<Self>,
+    elem: OsString,
+    children: RefCell<FxHashMap<OsString, Rc<Self>>>,
+    elems: RefCell<FxHashSet<OsString>>,
+}
 
-            crate::markdown::render(&index_page, md_opts, cx.shared.edition())
-                .map_err(|e| Error::new(e, &index_page))?;
-        } else {
-            let shared = Rc::clone(&cx.shared);
-            let dst = cx.dst.join("index.html");
-            let page = layout::Page {
-                title: "Index of crates",
-                css_class: "mod sys",
-                root_path: "./",
-                static_root_path: shared.static_root_path.as_deref(),
-                description: "List of crates",
-                resource_suffix: &shared.resource_suffix,
-                rust_logo: true,
-            };
+impl Hierarchy {
+    fn with_parent(elem: OsString, parent: &Rc<Self>) -> Self {
+        Self { elem, parent: Rc::downgrade(parent), ..Self::default() }
+    }
 
-            let content = format!(
-                "<h1>List of all crates</h1><ul class=\"all-items\">{}</ul>",
-                krates.iter().format_with("", |k, f| {
-                    f(&format_args!(
-                        "<li><a href=\"{trailing_slash}index.html\">{k}</a></li>",
-                        trailing_slash = ensure_trailing_slash(k),
-                    ))
-                })
-            );
-            let v = layout::render(&shared.layout, &page, "", content, &shared.style_files);
-            shared.fs.write(dst, v)?;
+    fn to_json_string(&self) -> SortedJson {
+        let subs = self.children.borrow();
+        let files = self.elems.borrow();
+        let name = SortedJson::serialize(self.elem.to_str().expect("invalid osstring conversion"));
+        let mut out = Vec::from([name]);
+        if !subs.is_empty() || !files.is_empty() {
+            let subs = subs.iter().map(|(_, s)| s.to_json_string());
+            out.push(SortedJson::array(subs));
         }
+        if !files.is_empty() {
+            let files =
+                files.iter().map(|s| SortedJson::serialize(s.to_str().expect("invalid osstring")));
+            out.push(SortedJson::array(files));
+        }
+        SortedJson::array_unsorted(out)
     }
 
-    let cloned_shared = Rc::clone(&cx.shared);
-    let cache = &cloned_shared.cache;
-
-    // Collect the list of aliased types and their aliases.
-    // <https://github.com/search?q=repo%3Arust-lang%2Frust+[RUSTDOCIMPL]+type.impl&type=code>
-    //
-    // The clean AST has type aliases that point at their types, but
-    // this visitor works to reverse that: `aliased_types` is a map
-    // from target to the aliases that reference it, and each one
-    // will generate one file.
-    struct TypeImplCollector<'cx, 'cache> {
-        // Map from DefId-of-aliased-type to its data.
-        aliased_types: IndexMap<DefId, AliasedType<'cache>>,
-        visited_aliases: FxHashSet<DefId>,
-        cache: &'cache Cache,
-        cx: &'cache mut Context<'cx>,
-    }
-    // Data for an aliased type.
-    //
-    // In the final file, the format will be roughly:
-    //
-    // ```json
-    // // type.impl/CRATE/TYPENAME.js
-    // JSONP(
-    // "CRATE": [
-    //   ["IMPL1 HTML", "ALIAS1", "ALIAS2", ...],
-    //   ["IMPL2 HTML", "ALIAS3", "ALIAS4", ...],
-    //    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ struct AliasedType
-    //   ...
-    // ]
-    // )
-    // ```
-    struct AliasedType<'cache> {
-        // This is used to generate the actual filename of this aliased type.
-        target_fqp: &'cache [Symbol],
-        target_type: ItemType,
-        // This is the data stored inside the file.
-        // ItemId is used to deduplicate impls.
-        impl_: IndexMap<ItemId, AliasedTypeImpl<'cache>>,
-    }
-    // The `impl_` contains data that's used to figure out if an alias will work,
-    // and to generate the HTML at the end.
-    //
-    // The `type_aliases` list is built up with each type alias that matches.
-    struct AliasedTypeImpl<'cache> {
-        impl_: &'cache Impl,
-        type_aliases: Vec<(&'cache [Symbol], Item)>,
-    }
-    impl<'cx, 'cache> DocVisitor for TypeImplCollector<'cx, 'cache> {
-        fn visit_item(&mut self, it: &Item) {
-            self.visit_item_recur(it);
-            let cache = self.cache;
-            let ItemKind::TypeAliasItem(ref t) = *it.kind else { return };
-            let Some(self_did) = it.item_id.as_def_id() else { return };
-            if !self.visited_aliases.insert(self_did) {
-                return;
-            }
-            let Some(target_did) = t.type_.def_id(cache) else { return };
-            let get_extern = { || cache.external_paths.get(&target_did) };
-            let Some(&(ref target_fqp, target_type)) =
-                cache.paths.get(&target_did).or_else(get_extern)
-            else {
-                return;
-            };
-            let aliased_type = self.aliased_types.entry(target_did).or_insert_with(|| {
-                let impl_ = cache
-                    .impls
-                    .get(&target_did)
-                    .map(|v| &v[..])
-                    .unwrap_or_default()
-                    .iter()
-                    .map(|impl_| {
-                        (
-                            impl_.impl_item.item_id,
-                            AliasedTypeImpl { impl_, type_aliases: Vec::new() },
-                        )
-                    })
-                    .collect();
-                AliasedType { target_fqp: &target_fqp[..], target_type, impl_ }
-            });
-            let get_local = { || cache.paths.get(&self_did).map(|(p, _)| p) };
-            let Some(self_fqp) = cache.exact_paths.get(&self_did).or_else(get_local) else {
-                return;
-            };
-            let aliased_ty = self.cx.tcx().type_of(self_did).skip_binder();
-            // Exclude impls that are directly on this type. They're already in the HTML.
-            // Some inlining scenarios can cause there to be two versions of the same
-            // impl: one on the type alias and one on the underlying target type.
-            let mut seen_impls: FxHashSet<ItemId> = cache
-                .impls
-                .get(&self_did)
-                .map(|s| &s[..])
-                .unwrap_or_default()
-                .iter()
-                .map(|i| i.impl_item.item_id)
-                .collect();
-            for (impl_item_id, aliased_type_impl) in &mut aliased_type.impl_ {
-                // Only include this impl if it actually unifies with this alias.
-                // Synthetic impls are not included; those are also included in the HTML.
-                //
-                // FIXME(lazy_type_alias): Once the feature is complete or stable, rewrite this
-                // to use type unification.
-                // Be aware of `tests/rustdoc/type-alias/deeply-nested-112515.rs` which might regress.
-                let Some(impl_did) = impl_item_id.as_def_id() else { continue };
-                let for_ty = self.cx.tcx().type_of(impl_did).skip_binder();
-                let reject_cx = DeepRejectCtxt::new(self.cx.tcx(), TreatParams::AsCandidateKey);
-                if !reject_cx.types_may_unify(aliased_ty, for_ty) {
-                    continue;
-                }
-                // Avoid duplicates
-                if !seen_impls.insert(*impl_item_id) {
-                    continue;
+    fn add_path(self: &Rc<Self>, path: &Path) {
+        let mut h = Rc::clone(&self);
+        let mut elems = path
+            .components()
+            .filter_map(|s| match s {
+                Component::Normal(s) => Some(s.to_owned()),
+                Component::ParentDir => Some(OsString::from("..")),
+                _ => None,
+            })
+            .peekable();
+        loop {
+            let cur_elem = elems.next().expect("empty file path");
+            if cur_elem == ".." {
+                if let Some(parent) = h.parent.upgrade() {
+                    h = parent;
                 }
-                // This impl was not found in the set of rejected impls
-                aliased_type_impl.type_aliases.push((&self_fqp[..], it.clone()));
+                continue;
             }
-        }
-    }
-    let mut type_impl_collector = TypeImplCollector {
-        aliased_types: IndexMap::default(),
-        visited_aliases: FxHashSet::default(),
-        cache,
-        cx,
-    };
-    DocVisitor::visit_crate(&mut type_impl_collector, &krate);
-    // Final serialized form of the alias impl
-    struct AliasSerializableImpl {
-        text: String,
-        trait_: Option<String>,
-        aliases: Vec<String>,
-    }
-    impl Serialize for AliasSerializableImpl {
-        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-        where
-            S: Serializer,
-        {
-            let mut seq = serializer.serialize_seq(None)?;
-            seq.serialize_element(&self.text)?;
-            if let Some(trait_) = &self.trait_ {
-                seq.serialize_element(trait_)?;
+            if elems.peek().is_none() {
+                h.elems.borrow_mut().insert(cur_elem);
+                break;
             } else {
-                seq.serialize_element(&0)?;
-            }
-            for type_ in &self.aliases {
-                seq.serialize_element(type_)?;
+                let entry = Rc::clone(
+                    h.children
+                        .borrow_mut()
+                        .entry(cur_elem.clone())
+                        .or_insert_with(|| Rc::new(Self::with_parent(cur_elem, &h))),
+                );
+                h = entry;
             }
-            seq.end()
         }
     }
-    let cx = type_impl_collector.cx;
-    let dst = cx.dst.join("type.impl");
-    let aliased_types = type_impl_collector.aliased_types;
-    for aliased_type in aliased_types.values() {
-        let impls = aliased_type
-            .impl_
-            .values()
-            .flat_map(|AliasedTypeImpl { impl_, type_aliases }| {
-                let mut ret = Vec::new();
-                let trait_ = impl_
-                    .inner_impl()
-                    .trait_
-                    .as_ref()
-                    .map(|trait_| format!("{:#}", trait_.print(cx)));
-                // render_impl will filter out "impossible-to-call" methods
-                // to make that functionality work here, it needs to be called with
-                // each type alias, and if it gives a different result, split the impl
-                for &(type_alias_fqp, ref type_alias_item) in type_aliases {
-                    let mut buf = Buffer::html();
-                    cx.id_map = Default::default();
-                    cx.deref_id_map = Default::default();
-                    let target_did = impl_
+}
+
+#[derive(Serialize, Deserialize, Clone, Default, Debug)]
+struct TypeAlias;
+type TypeAliasPart = Part<TypeAlias, SortedJson>;
+impl CciPart for TypeAliasPart {
+    type FileFormat = sorted_template::Js;
+}
+
+impl TypeAliasPart {
+    fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
+        SortedTemplate::before_after(
+            r"(function() {
+    var type_impls = Object.fromEntries([",
+            r"]);
+    if (window.register_type_impls) {
+        window.register_type_impls(type_impls);
+    } else {
+        window.pending_type_impls = type_impls;
+    }
+})()",
+        )
+    }
+
+    fn get(
+        cx: &mut Context<'_>,
+        krate: &Crate,
+        crate_name_json: &SortedJson,
+    ) -> Result<PartsAndLocations<Self>, Error> {
+        let cache = &Rc::clone(&cx.shared).cache;
+        let mut path_parts = PartsAndLocations::default();
+
+        let mut type_impl_collector = TypeImplCollector {
+            aliased_types: IndexMap::default(),
+            visited_aliases: FxHashSet::default(),
+            cache,
+            cx,
+        };
+        DocVisitor::visit_crate(&mut type_impl_collector, &krate);
+        let cx = type_impl_collector.cx;
+        let aliased_types = type_impl_collector.aliased_types;
+        for aliased_type in aliased_types.values() {
+            let impls = aliased_type
+                .impl_
+                .values()
+                .flat_map(|AliasedTypeImpl { impl_, type_aliases }| {
+                    let mut ret = Vec::new();
+                    let trait_ = impl_
                         .inner_impl()
                         .trait_
                         .as_ref()
-                        .map(|trait_| trait_.def_id())
-                        .or_else(|| impl_.inner_impl().for_.def_id(cache));
-                    let provided_methods;
-                    let assoc_link = if let Some(target_did) = target_did {
-                        provided_methods = impl_.inner_impl().provided_trait_methods(cx.tcx());
-                        AssocItemLink::GotoSource(ItemId::DefId(target_did), &provided_methods)
-                    } else {
-                        AssocItemLink::Anchor(None)
-                    };
-                    super::render_impl(
-                        &mut buf,
-                        cx,
-                        *impl_,
-                        &type_alias_item,
-                        assoc_link,
-                        RenderMode::Normal,
-                        None,
-                        &[],
-                        ImplRenderingParameters {
-                            show_def_docs: true,
-                            show_default_items: true,
-                            show_non_assoc_items: true,
-                            toggle_open_by_default: true,
-                        },
-                    );
-                    let text = buf.into_inner();
-                    let type_alias_fqp = (*type_alias_fqp).iter().join("::");
-                    if Some(&text) == ret.last().map(|s: &AliasSerializableImpl| &s.text) {
-                        ret.last_mut()
-                            .expect("already established that ret.last() is Some()")
-                            .aliases
-                            .push(type_alias_fqp);
+                        .map(|trait_| format!("{:#}", trait_.print(cx)));
+                    // render_impl will filter out "impossible-to-call" methods
+                    // to make that functionality work here, it needs to be called with
+                    // each type alias, and if it gives a different result, split the impl
+                    for &(type_alias_fqp, ref type_alias_item) in type_aliases {
+                        let mut buf = Buffer::html();
+                        cx.id_map = Default::default();
+                        cx.deref_id_map = Default::default();
+                        let target_did = impl_
+                            .inner_impl()
+                            .trait_
+                            .as_ref()
+                            .map(|trait_| trait_.def_id())
+                            .or_else(|| impl_.inner_impl().for_.def_id(cache));
+                        let provided_methods;
+                        let assoc_link = if let Some(target_did) = target_did {
+                            provided_methods = impl_.inner_impl().provided_trait_methods(cx.tcx());
+                            AssocItemLink::GotoSource(ItemId::DefId(target_did), &provided_methods)
+                        } else {
+                            AssocItemLink::Anchor(None)
+                        };
+                        super::render_impl(
+                            &mut buf,
+                            cx,
+                            *impl_,
+                            &type_alias_item,
+                            assoc_link,
+                            RenderMode::Normal,
+                            None,
+                            &[],
+                            ImplRenderingParameters {
+                                show_def_docs: true,
+                                show_default_items: true,
+                                show_non_assoc_items: true,
+                                toggle_open_by_default: true,
+                            },
+                        );
+                        let text = buf.into_inner();
+                        let type_alias_fqp = (*type_alias_fqp).iter().join("::");
+                        if Some(&text) == ret.last().map(|s: &AliasSerializableImpl| &s.text) {
+                            ret.last_mut()
+                                .expect("already established that ret.last() is Some()")
+                                .aliases
+                                .push(type_alias_fqp);
+                        } else {
+                            ret.push(AliasSerializableImpl {
+                                text,
+                                trait_: trait_.clone(),
+                                aliases: vec![type_alias_fqp],
+                            })
+                        }
+                    }
+                    ret
+                })
+                .collect::<Vec<_>>();
+
+            let mut path = PathBuf::from("type.impl");
+            for component in &aliased_type.target_fqp[..aliased_type.target_fqp.len() - 1] {
+                path.push(component.as_str());
+            }
+            let aliased_item_type = aliased_type.target_type;
+            path.push(&format!(
+                "{aliased_item_type}.{}.js",
+                aliased_type.target_fqp[aliased_type.target_fqp.len() - 1]
+            ));
+
+            let part =
+                SortedJson::array(impls.iter().map(SortedJson::serialize).collect::<Vec<_>>());
+            path_parts.push(path, SortedJson::array_unsorted([crate_name_json, &part]));
+        }
+        Ok(path_parts)
+    }
+}
+
+#[derive(Serialize, Deserialize, Clone, Default, Debug)]
+struct TraitAlias;
+type TraitAliasPart = Part<TraitAlias, SortedJson>;
+impl CciPart for TraitAliasPart {
+    type FileFormat = sorted_template::Js;
+}
+
+impl TraitAliasPart {
+    fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
+        SortedTemplate::before_after(
+            r"(function() {
+    var implementors = Object.fromEntries([",
+            r"]);
+    if (window.register_implementors) {
+        window.register_implementors(implementors);
+    } else {
+        window.pending_implementors = implementors;
+    }
+})()",
+        )
+    }
+
+    fn get(
+        cx: &mut Context<'_>,
+        crate_name_json: &SortedJson,
+    ) -> Result<PartsAndLocations<Self>, Error> {
+        let cache = &cx.shared.cache;
+        let mut path_parts = PartsAndLocations::default();
+        // Update the list of all implementors for traits
+        // <https://github.com/search?q=repo%3Arust-lang%2Frust+[RUSTDOCIMPL]+trait.impl&type=code>
+        for (&did, imps) in &cache.implementors {
+            // Private modules can leak through to this phase of rustdoc, which
+            // could contain implementations for otherwise private types. In some
+            // rare cases we could find an implementation for an item which wasn't
+            // indexed, so we just skip this step in that case.
+            //
+            // FIXME: this is a vague explanation for why this can't be a `get`, in
+            //        theory it should be...
+            let (remote_path, remote_item_type) = match cache.exact_paths.get(&did) {
+                Some(p) => match cache.paths.get(&did).or_else(|| cache.external_paths.get(&did)) {
+                    Some((_, t)) => (p, t),
+                    None => continue,
+                },
+                None => match cache.external_paths.get(&did) {
+                    Some((p, t)) => (p, t),
+                    None => continue,
+                },
+            };
+
+            let implementors = imps
+                .iter()
+                .filter_map(|imp| {
+                    // If the trait and implementation are in the same crate, then
+                    // there's no need to emit information about it (there's inlining
+                    // going on). If they're in different crates then the crate defining
+                    // the trait will be interested in our implementation.
+                    //
+                    // If the implementation is from another crate then that crate
+                    // should add it.
+                    if imp.impl_item.item_id.krate() == did.krate
+                        || !imp.impl_item.item_id.is_local()
+                    {
+                        None
                     } else {
-                        ret.push(AliasSerializableImpl {
-                            text,
-                            trait_: trait_.clone(),
-                            aliases: vec![type_alias_fqp],
+                        Some(Implementor {
+                            text: imp.inner_impl().print(false, cx).to_string(),
+                            synthetic: imp.inner_impl().kind.is_auto(),
+                            types: collect_paths_for_type(imp.inner_impl().for_.clone(), cache),
                         })
                     }
-                }
-                ret
-            })
-            .collect::<Vec<_>>();
+                })
+                .collect::<Vec<_>>();
 
-        // FIXME: this fixes only rustdoc part of instability of trait impls
-        // for js files, see #120371
-        // Manually collect to string and sort to make list not depend on order
-        let mut impls = impls
-            .iter()
-            .map(|i| serde_json::to_string(i).expect("failed serde conversion"))
-            .collect::<Vec<_>>();
-        impls.sort();
+            // Only create a js file if we have impls to add to it. If the trait is
+            // documented locally though we always create the file to avoid dead
+            // links.
+            if implementors.is_empty() && !cache.paths.contains_key(&did) {
+                continue;
+            }
 
-        let impls = format!(r#""{}":[{}]"#, krate.name(cx.tcx()), impls.join(","));
+            let mut path = PathBuf::from("trait.impl");
+            for component in &remote_path[..remote_path.len() - 1] {
+                path.push(component.as_str());
+            }
+            path.push(&format!("{remote_item_type}.{}.js", remote_path[remote_path.len() - 1]));
 
-        let mut mydst = dst.clone();
-        for part in &aliased_type.target_fqp[..aliased_type.target_fqp.len() - 1] {
-            mydst.push(part.to_string());
+            let part = SortedJson::array(
+                implementors.iter().map(SortedJson::serialize).collect::<Vec<_>>(),
+            );
+            path_parts.push(path, SortedJson::array_unsorted([crate_name_json, &part]));
         }
-        cx.shared.ensure_dir(&mydst)?;
-        let aliased_item_type = aliased_type.target_type;
-        mydst.push(&format!(
-            "{aliased_item_type}.{}.js",
-            aliased_type.target_fqp[aliased_type.target_fqp.len() - 1]
-        ));
-
-        let (mut all_impls, _) = try_err!(collect(&mydst, krate.name(cx.tcx()).as_str()), &mydst);
-        all_impls.push(impls);
-        // Sort the implementors by crate so the file will be generated
-        // identically even with rustdoc running in parallel.
-        all_impls.sort();
-
-        let mut v = String::from("(function() {var type_impls = {\n");
-        v.push_str(&all_impls.join(",\n"));
-        v.push_str("\n};");
-        v.push_str(
-            "if (window.register_type_impls) {\
-                 window.register_type_impls(type_impls);\
-             } else {\
-                 window.pending_type_impls = type_impls;\
-             }",
-        );
-        v.push_str("})()");
-        cx.shared.fs.write(mydst, v)?;
-    }
-
-    // Update the list of all implementors for traits
-    // <https://github.com/search?q=repo%3Arust-lang%2Frust+[RUSTDOCIMPL]+trait.impl&type=code>
-    let dst = cx.dst.join("trait.impl");
-    for (&did, imps) in &cache.implementors {
-        // Private modules can leak through to this phase of rustdoc, which
-        // could contain implementations for otherwise private types. In some
-        // rare cases we could find an implementation for an item which wasn't
-        // indexed, so we just skip this step in that case.
-        //
-        // FIXME: this is a vague explanation for why this can't be a `get`, in
-        //        theory it should be...
-        let (remote_path, remote_item_type) = match cache.exact_paths.get(&did) {
-            Some(p) => match cache.paths.get(&did).or_else(|| cache.external_paths.get(&did)) {
-                Some((_, t)) => (p, t),
-                None => continue,
-            },
-            None => match cache.external_paths.get(&did) {
-                Some((p, t)) => (p, t),
-                None => continue,
-            },
-        };
+        Ok(path_parts)
+    }
+}
 
-        struct Implementor {
-            text: String,
-            synthetic: bool,
-            types: Vec<String>,
+struct Implementor {
+    text: String,
+    synthetic: bool,
+    types: Vec<String>,
+}
+
+impl Serialize for Implementor {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut seq = serializer.serialize_seq(None)?;
+        seq.serialize_element(&self.text)?;
+        if self.synthetic {
+            seq.serialize_element(&1)?;
+            seq.serialize_element(&self.types)?;
         }
+        seq.end()
+    }
+}
 
-        impl Serialize for Implementor {
-            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-            where
-                S: Serializer,
-            {
-                let mut seq = serializer.serialize_seq(None)?;
-                seq.serialize_element(&self.text)?;
-                if self.synthetic {
-                    seq.serialize_element(&1)?;
-                    seq.serialize_element(&self.types)?;
-                }
-                seq.end()
+/// Collect the list of aliased types and their aliases.
+/// <https://github.com/search?q=repo%3Arust-lang%2Frust+[RUSTDOCIMPL]+type.impl&type=code>
+///
+/// The clean AST has type aliases that point at their types, but
+/// this visitor works to reverse that: `aliased_types` is a map
+/// from target to the aliases that reference it, and each one
+/// will generate one file.
+struct TypeImplCollector<'cx, 'cache> {
+    /// Map from DefId-of-aliased-type to its data.
+    aliased_types: IndexMap<DefId, AliasedType<'cache>>,
+    visited_aliases: FxHashSet<DefId>,
+    cache: &'cache Cache,
+    cx: &'cache mut Context<'cx>,
+}
+
+/// Data for an aliased type.
+///
+/// In the final file, the format will be roughly:
+///
+/// ```json
+/// // type.impl/CRATE/TYPENAME.js
+/// JSONP(
+/// "CRATE": [
+///   ["IMPL1 HTML", "ALIAS1", "ALIAS2", ...],
+///   ["IMPL2 HTML", "ALIAS3", "ALIAS4", ...],
+///    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ struct AliasedType
+///   ...
+/// ]
+/// )
+/// ```
+struct AliasedType<'cache> {
+    /// This is used to generate the actual filename of this aliased type.
+    target_fqp: &'cache [Symbol],
+    target_type: ItemType,
+    /// This is the data stored inside the file.
+    /// ItemId is used to deduplicate impls.
+    impl_: IndexMap<ItemId, AliasedTypeImpl<'cache>>,
+}
+
+/// The `impl_` contains data that's used to figure out if an alias will work,
+/// and to generate the HTML at the end.
+///
+/// The `type_aliases` list is built up with each type alias that matches.
+struct AliasedTypeImpl<'cache> {
+    impl_: &'cache Impl,
+    type_aliases: Vec<(&'cache [Symbol], Item)>,
+}
+
+impl<'cx, 'cache> DocVisitor for TypeImplCollector<'cx, 'cache> {
+    fn visit_item(&mut self, it: &Item) {
+        self.visit_item_recur(it);
+        let cache = self.cache;
+        let ItemKind::TypeAliasItem(ref t) = *it.kind else { return };
+        let Some(self_did) = it.item_id.as_def_id() else { return };
+        if !self.visited_aliases.insert(self_did) {
+            return;
+        }
+        let Some(target_did) = t.type_.def_id(cache) else { return };
+        let get_extern = { || cache.external_paths.get(&target_did) };
+        let Some(&(ref target_fqp, target_type)) = cache.paths.get(&target_did).or_else(get_extern)
+        else {
+            return;
+        };
+        let aliased_type = self.aliased_types.entry(target_did).or_insert_with(|| {
+            let impl_ = cache
+                .impls
+                .get(&target_did)
+                .map(|v| &v[..])
+                .unwrap_or_default()
+                .iter()
+                .map(|impl_| {
+                    (impl_.impl_item.item_id, AliasedTypeImpl { impl_, type_aliases: Vec::new() })
+                })
+                .collect();
+            AliasedType { target_fqp: &target_fqp[..], target_type, impl_ }
+        });
+        let get_local = { || cache.paths.get(&self_did).map(|(p, _)| p) };
+        let Some(self_fqp) = cache.exact_paths.get(&self_did).or_else(get_local) else {
+            return;
+        };
+        let aliased_ty = self.cx.tcx().type_of(self_did).skip_binder();
+        // Exclude impls that are directly on this type. They're already in the HTML.
+        // Some inlining scenarios can cause there to be two versions of the same
+        // impl: one on the type alias and one on the underlying target type.
+        let mut seen_impls: FxHashSet<ItemId> = cache
+            .impls
+            .get(&self_did)
+            .map(|s| &s[..])
+            .unwrap_or_default()
+            .iter()
+            .map(|i| i.impl_item.item_id)
+            .collect();
+        for (impl_item_id, aliased_type_impl) in &mut aliased_type.impl_ {
+            // Only include this impl if it actually unifies with this alias.
+            // Synthetic impls are not included; those are also included in the HTML.
+            //
+            // FIXME(lazy_type_alias): Once the feature is complete or stable, rewrite this
+            // to use type unification.
+            // Be aware of `tests/rustdoc/type-alias/deeply-nested-112515.rs` which might regress.
+            let Some(impl_did) = impl_item_id.as_def_id() else { continue };
+            let for_ty = self.cx.tcx().type_of(impl_did).skip_binder();
+            let reject_cx = DeepRejectCtxt::new(self.cx.tcx(), TreatParams::AsCandidateKey);
+            if !reject_cx.types_may_unify(aliased_ty, for_ty) {
+                continue;
+            }
+            // Avoid duplicates
+            if !seen_impls.insert(*impl_item_id) {
+                continue;
             }
+            // This impl was not found in the set of rejected impls
+            aliased_type_impl.type_aliases.push((&self_fqp[..], it.clone()));
         }
+    }
+}
 
-        let implementors = imps
-            .iter()
-            .filter_map(|imp| {
-                // If the trait and implementation are in the same crate, then
-                // there's no need to emit information about it (there's inlining
-                // going on). If they're in different crates then the crate defining
-                // the trait will be interested in our implementation.
-                //
-                // If the implementation is from another crate then that crate
-                // should add it.
-                if imp.impl_item.item_id.krate() == did.krate || !imp.impl_item.item_id.is_local() {
-                    None
-                } else {
-                    Some(Implementor {
-                        text: imp.inner_impl().print(false, cx).to_string(),
-                        synthetic: imp.inner_impl().kind.is_auto(),
-                        types: collect_paths_for_type(imp.inner_impl().for_.clone(), cache),
-                    })
-                }
-            })
-            .collect::<Vec<_>>();
+/// Final serialized form of the alias impl
+struct AliasSerializableImpl {
+    text: String,
+    trait_: Option<String>,
+    aliases: Vec<String>,
+}
 
-        // Only create a js file if we have impls to add to it. If the trait is
-        // documented locally though we always create the file to avoid dead
-        // links.
-        if implementors.is_empty() && !cache.paths.contains_key(&did) {
-            continue;
+impl Serialize for AliasSerializableImpl {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut seq = serializer.serialize_seq(None)?;
+        seq.serialize_element(&self.text)?;
+        if let Some(trait_) = &self.trait_ {
+            seq.serialize_element(trait_)?;
+        } else {
+            seq.serialize_element(&0)?;
+        }
+        for type_ in &self.aliases {
+            seq.serialize_element(type_)?;
         }
+        seq.end()
+    }
+}
 
-        // FIXME: this fixes only rustdoc part of instability of trait impls
-        // for js files, see #120371
-        // Manually collect to string and sort to make list not depend on order
-        let mut implementors = implementors
-            .iter()
-            .map(|i| serde_json::to_string(i).expect("failed serde conversion"))
-            .collect::<Vec<_>>();
-        implementors.sort();
+/// Create all parents
+fn create_parents(path: &Path) -> Result<(), Error> {
+    let parent = path.parent().expect("should not have an empty path here");
+    try_err!(fs::create_dir_all(parent), parent);
+    Ok(())
+}
 
-        let implementors = format!(r#""{}":[{}]"#, krate.name(cx.tcx()), implementors.join(","));
+/// Create parents and then write
+fn write_create_parents(path: &Path, content: String) -> Result<(), Error> {
+    create_parents(path)?;
+    try_err!(fs::write(path, content), path);
+    Ok(())
+}
 
-        let mut mydst = dst.clone();
-        for part in &remote_path[..remote_path.len() - 1] {
-            mydst.push(part.to_string());
+/// Returns a blank template unless we could find one to append to
+fn read_template_or_blank<F, T: CciPart>(
+    mut make_blank: F,
+    path: &Path,
+) -> Result<SortedTemplate<T::FileFormat>, Error>
+where
+    F: FnMut() -> SortedTemplate<T::FileFormat>,
+{
+    match fs::read_to_string(&path) {
+        Ok(template) => Ok(try_err!(SortedTemplate::from_str(&template), &path)),
+        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(make_blank()),
+        Err(e) => Err(Error::new(e, &path)),
+    }
+}
+
+/// info from this crate and the --include-info-json'd crates
+fn write_rendered_cci<T: CciPart, F>(
+    mut make_blank: F,
+    dst: &Path,
+    crates_info: &[CrateInfo],
+) -> Result<(), Error>
+where
+    F: FnMut() -> SortedTemplate<T::FileFormat>,
+{
+    // read parts from disk
+    let path_parts =
+        crates_info.iter().map(|crate_info| crate_info.get::<T>().parts.iter()).flatten();
+    // read previous rendered cci from storage, append to them
+    let mut templates: FxHashMap<PathBuf, SortedTemplate<T::FileFormat>> = Default::default();
+    for (path, part) in path_parts {
+        let part = format!("{part}");
+        let path = dst.join(&path);
+        match templates.entry(path.clone()) {
+            Entry::Vacant(entry) => {
+                let template = read_template_or_blank::<_, T>(&mut make_blank, &path)?;
+                let template = entry.insert(template);
+                template.append(part);
+            }
+            Entry::Occupied(mut t) => t.get_mut().append(part),
         }
-        cx.shared.ensure_dir(&mydst)?;
-        mydst.push(&format!("{remote_item_type}.{}.js", remote_path[remote_path.len() - 1]));
-
-        let (mut all_implementors, _) =
-            try_err!(collect(&mydst, krate.name(cx.tcx()).as_str()), &mydst);
-        all_implementors.push(implementors);
-        // Sort the implementors by crate so the file will be generated
-        // identically even with rustdoc running in parallel.
-        all_implementors.sort();
-
-        let mut v = String::from("(function() {var implementors = {\n");
-        v.push_str(&all_implementors.join(",\n"));
-        v.push_str("\n};");
-        v.push_str(
-            "if (window.register_implementors) {\
-                 window.register_implementors(implementors);\
-             } else {\
-                 window.pending_implementors = implementors;\
-             }",
-        );
-        v.push_str("})()");
-        cx.shared.fs.write(mydst, v)?;
+    }
+
+    // write the merged cci to disk
+    for (path, template) in templates {
+        create_parents(&path)?;
+        let file = try_err!(File::create(&path), &path);
+        let mut file = BufWriter::new(file);
+        try_err!(write!(file, "{template}"), &path);
+        try_err!(file.flush(), &path);
     }
     Ok(())
 }