about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2024-08-20 20:23:29 +0000
committerbors <bors@rust-lang.org>2024-08-20 20:23:29 +0000
commit5aea14073eee9e403c3bb857490cd6aa4a395531 (patch)
tree854dd2babf812c38433f0c1db2f00dedb8489b46
parent4d5b3b196284aded6ae99d12bcf149ffdc8ef379 (diff)
parentb4f057f01ddb414d250c0a70d1feb966cdfa9d99 (diff)
downloadrust-5aea14073eee9e403c3bb857490cd6aa4a395531.tar.gz
rust-5aea14073eee9e403c3bb857490cd6aa4a395531.zip
Auto merge of #128252 - EtomicBomb:pre-rfc, r=notriddle
modularize rustdoc's write_shared

Refactor src/librustdoc/html/render/write_shared.rs to reduce code duplication, adding unit tests

* Extract + unit test code for sorting and rendering JSON, which is duplicated 9 times in the current impl
* Extract + unit test code for encoding JSON as single quoted strings, which is duplicated twice in the current impl
* Unit tests for cross-crate information file formats
* Generic interface to add new kinds of cross-crate information files in the future
* Intended to match current behavior exactly, except for a merge info comment it adds to the bottom of cci files
* This PR is intended to reduce the review burden from my [mergeable rustdoc rfc](https://github.com/rust-lang/rfcs/pull/3662) implementation PR, which is a [small commit based on this branch](https://github.com/EtomicBomb/rust/tree/rfc). This code is agnostic to the RFC and does not include any of the flags discussed there, but cleanly enables the addition of these flags in the future because it is more modular
-rw-r--r--src/librustdoc/html/render/context.rs9
-rw-r--r--src/librustdoc/html/render/mod.rs2
-rw-r--r--src/librustdoc/html/render/ordered_json.rs83
-rw-r--r--src/librustdoc/html/render/ordered_json/tests.rs121
-rw-r--r--src/librustdoc/html/render/search_index.rs34
-rw-r--r--src/librustdoc/html/render/sorted_template.rs159
-rw-r--r--src/librustdoc/html/render/sorted_template/tests.rs149
-rw-r--r--src/librustdoc/html/render/write_shared.rs1470
-rw-r--r--src/librustdoc/html/render/write_shared/tests.rs207
9 files changed, 1551 insertions, 683 deletions
diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs
index 0334eacc161..0ed8921b1e8 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))
diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs
index 7ce637d3ab4..c1b2ee7d8ae 100644
--- a/src/librustdoc/html/render/mod.rs
+++ b/src/librustdoc/html/render/mod.rs
@@ -29,8 +29,10 @@ pub(crate) mod search_index;
 mod tests;
 
 mod context;
+mod ordered_json;
 mod print_item;
 pub(crate) mod sidebar;
+mod sorted_template;
 mod span_map;
 mod type_layout;
 mod write_shared;
diff --git a/src/librustdoc/html/render/ordered_json.rs b/src/librustdoc/html/render/ordered_json.rs
new file mode 100644
index 00000000000..7abe40eef3b
--- /dev/null
+++ b/src/librustdoc/html/render/ordered_json.rs
@@ -0,0 +1,83 @@
+use std::borrow::Borrow;
+use std::fmt;
+
+use itertools::Itertools as _;
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+
+/// Prerendered json.
+///
+/// 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 OrderedJson(String);
+
+impl OrderedJson {
+    /// If you pass in an array, it will not be sorted.
+    pub(crate) fn serialize<T: Serialize>(item: T) -> Result<Self, serde_json::Error> {
+        Ok(Self(serde_json::to_string(&item)?))
+    }
+
+    /// Serializes and sorts
+    pub(crate) fn array_sorted<T: Borrow<Self>, 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()));
+        Self(format!("[{}]", items))
+    }
+
+    pub(crate) fn array_unsorted<T: Borrow<Self>, I: IntoIterator<Item = T>>(items: I) -> Self {
+        let items = items.into_iter().format_with(",", |item, f| f(item.borrow()));
+        Self(format!("[{items}]"))
+    }
+}
+
+impl fmt::Display for OrderedJson {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl From<Value> for OrderedJson {
+    fn from(value: Value) -> Self {
+        let serialized =
+            serde_json::to_string(&value).expect("Serializing a Value to String should never fail");
+        Self(serialized)
+    }
+}
+
+impl From<OrderedJson> for Value {
+    fn from(json: OrderedJson) -> Self {
+        serde_json::from_str(&json.0).expect("OrderedJson should always store valid JSON")
+    }
+}
+
+/// For use in JSON.parse('{...}').
+///
+/// Assumes we are going to be wrapped in single quoted strings.
+///
+/// JSON.parse loads faster than raw JS source,
+/// so this is used for large objects.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub(crate) struct EscapedJson(OrderedJson);
+
+impl From<OrderedJson> for EscapedJson {
+    fn from(json: OrderedJson) -> Self {
+        Self(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("\\\"", "\\\\\"");
+        json.fmt(f)
+    }
+}
+
+#[cfg(test)]
+mod tests;
diff --git a/src/librustdoc/html/render/ordered_json/tests.rs b/src/librustdoc/html/render/ordered_json/tests.rs
new file mode 100644
index 00000000000..e0fe6446b9a
--- /dev/null
+++ b/src/librustdoc/html/render/ordered_json/tests.rs
@@ -0,0 +1,121 @@
+use super::super::ordered_json::*;
+
+fn check(json: OrderedJson, serialized: &str) {
+    assert_eq!(json.to_string(), serialized);
+    assert_eq!(serde_json::to_string(&json).unwrap(), serialized);
+
+    let json = json.to_string();
+    let json: OrderedJson = 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: OrderedJson = serde_json::from_str(&json).unwrap();
+
+    assert_eq!(json.to_string(), serialized);
+    assert_eq!(serde_json::to_string(&json).unwrap(), serialized);
+}
+
+// Make sure there is no extra level of string, plus number of escapes.
+#[test]
+fn escape_json_number() {
+    let json = OrderedJson::serialize(3).unwrap();
+    let json = EscapedJson::from(json);
+    assert_eq!(format!("{json}"), "3");
+}
+
+#[test]
+fn escape_json_single_quote() {
+    let json = OrderedJson::serialize("he's").unwrap();
+    let json = EscapedJson::from(json);
+    assert_eq!(format!("{json}"), r#""he\'s""#);
+}
+
+#[test]
+fn escape_json_array() {
+    let json = OrderedJson::serialize([1, 2, 3]).unwrap();
+    let json = EscapedJson::from(json);
+    assert_eq!(format!("{json}"), r#"[1,2,3]"#);
+}
+
+#[test]
+fn escape_json_string() {
+    let json = OrderedJson::serialize(r#"he"llo"#).unwrap();
+    let json = EscapedJson::from(json);
+    assert_eq!(format!("{json}"), r#""he\\\"llo""#);
+}
+
+#[test]
+fn escape_json_string_escaped() {
+    let json = OrderedJson::serialize(r#"he\"llo"#).unwrap();
+    let json = EscapedJson::from(json);
+    assert_eq!(format!("{json}"), r#""he\\\\\\\"llo""#);
+}
+
+#[test]
+fn escape_json_string_escaped_escaped() {
+    let json = OrderedJson::serialize(r#"he\\"llo"#).unwrap();
+    let json = EscapedJson::from(json);
+    assert_eq!(format!("{json}"), r#""he\\\\\\\\\\\"llo""#);
+}
+
+// Testing round trip + making sure there is no extra level of string
+#[test]
+fn number() {
+    let json = OrderedJson::serialize(3).unwrap();
+    let serialized = "3";
+    check(json, serialized);
+}
+
+#[test]
+fn boolean() {
+    let json = OrderedJson::serialize(true).unwrap();
+    let serialized = "true";
+    check(json, serialized);
+}
+
+#[test]
+fn string() {
+    let json = OrderedJson::serialize("he\"llo").unwrap();
+    let serialized = r#""he\"llo""#;
+    check(json, serialized);
+}
+
+#[test]
+fn serialize_array() {
+    let json = OrderedJson::serialize([3, 1, 2]).unwrap();
+    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<OrderedJson> =
+        items.into_iter().map(OrderedJson::serialize).collect::<Result<Vec<_>, _>>().unwrap();
+    let json = OrderedJson::array_sorted(items);
+    check(json, serialized);
+}
+
+#[test]
+fn nested_array() {
+    let a = OrderedJson::serialize(3).unwrap();
+    let b = OrderedJson::serialize(2).unwrap();
+    let c = OrderedJson::serialize(1).unwrap();
+    let d = OrderedJson::serialize([1, 3, 2]).unwrap();
+    let json = OrderedJson::array_sorted([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<OrderedJson> =
+        items.into_iter().map(OrderedJson::serialize).collect::<Result<Vec<_>, _>>().unwrap();
+    let json = OrderedJson::array_unsorted(items);
+    check(json, serialized);
+}
diff --git a/src/librustdoc/html/render/search_index.rs b/src/librustdoc/html/render/search_index.rs
index 8a2f31f7413..8a12bdef69b 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::ordered_json::OrderedJson;
 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: OrderedJson,
     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 = OrderedJson::array_unsorted([
+        OrderedJson::serialize(crate_name.as_str()).unwrap(),
+        OrderedJson::serialize(data).unwrap(),
+    ]);
     SerializedSearchIndex { index, desc }
 }
 
diff --git a/src/librustdoc/html/render/sorted_template.rs b/src/librustdoc/html/render/sorted_template.rs
new file mode 100644
index 00000000000..28f7766d7c7
--- /dev/null
+++ b/src/librustdoc/html/render/sorted_template.rs
@@ -0,0 +1,159 @@
+use std::collections::BTreeSet;
+use std::fmt::{self, Write as _};
+use std::marker::PhantomData;
+use std::str::FromStr;
+
+use itertools::{Itertools as _, Position};
+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,
+    fragments: 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
+    fragment_lengths: Vec<usize>,
+}
+
+impl<F> SortedTemplate<F> {
+    /// Generate this template from arbitary text.
+    /// Will insert wherever the substring `delimiter` can be found.
+    /// Errors if it does not appear exactly once.
+    pub(crate) fn from_template(template: &str, delimiter: &str) -> Result<Self, Error> {
+        let mut split = template.split(delimiter);
+        let before = split.next().ok_or(Error("delimiter should appear at least once"))?;
+        let after = split.next().ok_or(Error("delimiter should appear at least once"))?;
+        // not `split_once` because we want to check for too many occurrences
+        if split.next().is_some() {
+            return Err(Error("delimiter should appear at most once"));
+        }
+        Ok(Self::from_before_after(before, after))
+    }
+
+    /// Template will insert fragments between `before` and `after`
+    pub(crate) fn from_before_after<S: ToString, T: ToString>(before: S, after: T) -> Self {
+        let before = before.to_string();
+        let after = after.to_string();
+        Self { format: PhantomData, before, after, fragments: Default::default() }
+    }
+}
+
+impl<F> SortedTemplate<F> {
+    /// Adds this text to the template
+    pub(crate) fn append(&mut self, insert: String) {
+        self.fragments.insert(insert);
+    }
+}
+
+impl<F: FileFormat> fmt::Display for SortedTemplate<F> {
+    fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let mut fragment_lengths = Vec::default();
+        write!(f, "{}", self.before)?;
+        for (p, fragment) in self.fragments.iter().with_position() {
+            let mut f = DeltaWriter { inner: &mut f, delta: 0 };
+            let sep = if matches!(p, Position::First | Position::Only) { "" } else { F::SEPARATOR };
+            write!(f, "{}{}", sep, fragment)?;
+            fragment_lengths.push(f.delta);
+        }
+        let offset = Offset { start: self.before.len(), fragment_lengths };
+        let offset = serde_json::to_string(&offset).unwrap();
+        write!(f, "{}\n{}{}{}", self.after, F::COMMENT_START, offset, F::COMMENT_END)
+    }
+}
+
+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("invalid format: should have a newline on the last line"))?;
+        let offset = offset
+            .strip_prefix(F::COMMENT_START)
+            .ok_or(Error("last line expected to start with a comment"))?;
+        let offset = offset
+            .strip_suffix(F::COMMENT_END)
+            .ok_or(Error("last line expected to end with a comment"))?;
+        let offset: Offset = serde_json::from_str(&offset).map_err(|_| {
+            Error("could not find insertion location descriptor object on last line")
+        })?;
+        let (before, mut s) =
+            s.split_at_checked(offset.start).ok_or(Error("invalid start: out of bounds"))?;
+        let mut fragments = BTreeSet::default();
+        for (p, &index) in offset.fragment_lengths.iter().with_position() {
+            let (fragment, rest) =
+                s.split_at_checked(index).ok_or(Error("invalid fragment length: out of bounds"))?;
+            s = rest;
+            let sep = if matches!(p, Position::First | Position::Only) { "" } else { F::SEPARATOR };
+            let fragment = fragment
+                .strip_prefix(sep)
+                .ok_or(Error("invalid fragment length: expected to find separator here"))?;
+            fragments.insert(fragment.to_string());
+        }
+        Ok(Self {
+            format: PhantomData,
+            before: before.to_string(),
+            after: s.to_string(),
+            fragments,
+        })
+    }
+}
+
+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(&'static str);
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "invalid template: {}", self.0)
+    }
+}
+
+struct DeltaWriter<W> {
+    inner: W,
+    delta: usize,
+}
+
+impl<W: fmt::Write> fmt::Write for DeltaWriter<W> {
+    fn write_str(&mut self, s: &str) -> fmt::Result {
+        self.inner.write_str(s)?;
+        self.delta += s.len();
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests;
diff --git a/src/librustdoc/html/render/sorted_template/tests.rs b/src/librustdoc/html/render/sorted_template/tests.rs
new file mode 100644
index 00000000000..db057463005
--- /dev/null
+++ b/src/librustdoc/html/render/sorted_template/tests.rs
@@ -0,0 +1,149 @@
+use std::str::FromStr;
+
+use super::super::sorted_template::*;
+
+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>::from_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>::from_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>::from_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>::from_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>::from_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>::from_template("[#]", "#").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>::from_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>::from_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>::from_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..a18b7a252a4 100644
--- a/src/librustdoc/html/render/write_shared.rs
+++ b/src/librustdoc/html/render/write_shared.rs
@@ -1,19 +1,41 @@
+//! Rustdoc writes aut 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::cell::RefCell;
-use std::fs::{self, File};
-use std::io::prelude::*;
-use std::io::{self, BufReader};
-use std::path::{Component, Path};
+use std::ffi::OsString;
+use std::fs::File;
+use std::io::{self, BufWriter, 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};
 
 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::de::DeserializeOwned;
 use serde::ser::SerializeSeq;
-use serde::{Serialize, Serializer};
+use serde::{Deserialize, Serialize, Serializer};
 
 use super::{collect_paths_for_type, ensure_trailing_slash, Context, RenderMode};
 use crate::clean::{Crate, Item, ItemId, ItemKind};
@@ -24,53 +46,83 @@ use crate::formats::cache::Cache;
 use crate::formats::item_type::ItemType;
 use crate::formats::Impl;
 use crate::html::format::Buffer;
-use crate::html::render::search_index::SerializedSearchIndex;
+use crate::html::layout;
+use crate::html::render::ordered_json::{EscapedJson, OrderedJson};
+use crate::html::render::search_index::{build_index, SerializedSearchIndex};
+use crate::html::render::sorted_template::{self, FileFormat, 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 cross-crate information files, 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 = OrderedJson::serialize(crate_name).unwrap(); // "rand"
+    let external_crates = hack_get_external_crate_names(&cx.dst)?;
+    let info = CrateInfo {
+        src_files_js: SourcesPart::get(cx, &crate_name_json)?,
+        search_index_js: SearchIndexPart::get(index, &cx.shared.resource_suffix)?,
+        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 = vec![info]; // we have info from just one crate. rest will found in out dir
+
+    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)?;
+        }
+        write_rendered_cci::<SearchIndexPart, _>(SearchIndexPart::blank, dst, &crates)?;
+        write_rendered_cci::<AllCratesPart, _>(AllCratesPart::blank, dst, &crates)?;
+    }
+    write_rendered_cci::<TraitAliasPart, _>(TraitAliasPart::blank, dst, &crates)?;
+    write_rendered_cci::<TypeAliasPart, _>(TypeAliasPart::blank, dst, &crates)?;
+    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)?;
+        }
+        _ => {} // 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 +149,786 @@ 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 = OrderedJson::serialize(&crate_name).unwrap();
+    let path = PathBuf::from_iter([&cx.dst, Path::new("search.desc"), Path::new(&crate_name)]);
+    if 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 = OrderedJson::serialize(&part).unwrap();
+        let part = format!("searchState.loadedDescShard({encoded_crate_name}, {i}, {part})");
+        create_parents(&path)?;
+        try_err!(fs::write(&path, part), &path);
     }
+    Ok(())
+}
 
-    use std::ffi::OsString;
+/// 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>,
+}
+
+/// 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)>,
+}
 
-    #[derive(Debug, Default)]
-    struct Hierarchy {
-        parent: Weak<Self>,
-        elem: OsString,
-        children: RefCell<FxHashMap<OsString, Rc<Self>>>,
-        elems: RefCell<FxHashSet<OsString>>,
+impl<P> Default for PartsAndLocations<P> {
+    fn default() -> Self {
+        Self { parts: Vec::default() }
     }
+}
 
-    impl Hierarchy {
-        fn with_parent(elem: OsString, parent: &Rc<Self>) -> Self {
-            Self { elem, parent: Rc::downgrade(parent), ..Self::default() }
-        }
+impl<T, U> PartsAndLocations<Part<T, U>> {
+    fn push(&mut self, path: PathBuf, item: U) {
+        self.parts.push((path, Part { _artifact: PhantomData, item }));
+    }
 
-        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
-            )
-        }
+    /// Singleton part, one file
+    fn with(path: PathBuf, part: U) -> Self {
+        let mut ret = Self::default();
+        ret.push(path, part);
+        ret
+    }
+}
 
-        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;
-                }
-            }
+/// 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;
+    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self>;
+}
+
+#[derive(Serialize, Deserialize, Clone, Default, Debug)]
+struct SearchIndex;
+type SearchIndexPart = Part<SearchIndex, EscapedJson>;
+impl CciPart for SearchIndexPart {
+    type FileFormat = sorted_template::Js;
+    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
+        &crate_info.search_index_js
+    }
+}
+
+impl SearchIndexPart {
+    fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
+        SortedTemplate::from_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(
+        search_index: OrderedJson,
+        resource_suffix: &str,
+    ) -> Result<PartsAndLocations<Self>, Error> {
+        let path = suffix_path("search-index.js", 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, OrderedJson>;
+impl CciPart for AllCratesPart {
+    type FileFormat = sorted_template::Js;
+    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
+        &crate_info.all_crates
+    }
+}
+
+impl AllCratesPart {
+    fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
+        SortedTemplate::from_before_after("window.ALL_CRATES = [", "];")
+    }
+
+    fn get(crate_name_json: OrderedJson) -> 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(doc_root: &Path) -> Result<Vec<String>, Error> {
+    let path = doc_root.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;
+    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
+        &crate_info.crates_index
+    }
+}
+
+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 DELIMITER: &str = "\u{FFFC}"; // users are being naughty if they have this
+        let content =
+            format!("<h1>List of all crates</h1><ul class=\"all-items\">{DELIMITER}</ul>");
+        let template = layout::render(layout, &page, "", content, &style_files);
+        match SortedTemplate::from_template(&template, DELIMITER) {
+            Ok(template) => template,
+            Err(e) => panic!(
+                "Object Replacement Character (U+FFFC) should not appear in the --index-page: {e}"
+            ),
         }
     }
 
-    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;
+    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
+        &crate_info.src_files_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::from_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: &OrderedJson) -> 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 = OrderedJson::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) -> OrderedJson {
+        let subs = self.children.borrow();
+        let files = self.elems.borrow();
+        let name = OrderedJson::serialize(self.elem.to_str().expect("invalid osstring conversion"))
+            .unwrap();
+        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(OrderedJson::array_sorted(subs));
+        }
+        if !files.is_empty() {
+            let files = files
+                .iter()
+                .map(|s| OrderedJson::serialize(s.to_str().expect("invalid osstring")).unwrap());
+            out.push(OrderedJson::array_sorted(files));
         }
+        OrderedJson::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, OrderedJson>;
+impl CciPart for TypeAliasPart {
+    type FileFormat = sorted_template::Js;
+    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
+        &crate_info.type_impl
+    }
+}
+
+impl TypeAliasPart {
+    fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
+        SortedTemplate::from_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: &OrderedJson,
+    ) -> 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 = OrderedJson::array_sorted(
+                impls.iter().map(OrderedJson::serialize).collect::<Result<Vec<_>, _>>().unwrap(),
+            );
+            path_parts.push(path, OrderedJson::array_unsorted([crate_name_json, &part]));
+        }
+        Ok(path_parts)
+    }
+}
+
+#[derive(Serialize, Deserialize, Clone, Default, Debug)]
+struct TraitAlias;
+type TraitAliasPart = Part<TraitAlias, OrderedJson>;
+impl CciPart for TraitAliasPart {
+    type FileFormat = sorted_template::Js;
+    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
+        &crate_info.trait_impl
+    }
+}
+
+impl TraitAliasPart {
+    fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
+        SortedTemplate::from_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: &OrderedJson,
+    ) -> 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 = OrderedJson::array_sorted(
+                implementors
+                    .iter()
+                    .map(OrderedJson::serialize)
+                    .collect::<Result<Vec<_>, _>>()
+                    .unwrap(),
+            );
+            path_parts.push(path, OrderedJson::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();
+fn get_path_parts<T: CciPart>(
+    dst: &Path,
+    crates_info: &[CrateInfo],
+) -> FxHashMap<PathBuf, Vec<String>> {
+    let mut templates: FxHashMap<PathBuf, Vec<String>> = FxHashMap::default();
+    crates_info
+        .iter()
+        .map(|crate_info| T::from_crate_info(crate_info).parts.iter())
+        .flatten()
+        .for_each(|(path, part)| {
+            let path = dst.join(&path);
+            let part = part.to_string();
+            templates.entry(path).or_default().push(part);
+        });
+    templates
+}
 
-        let implementors = format!(r#""{}":[{}]"#, krate.name(cx.tcx()), implementors.join(","));
+/// 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(())
+}
+
+/// Returns a blank template unless we could find one to append to
+fn read_template_or_blank<F, T: FileFormat>(
+    mut make_blank: F,
+    path: &Path,
+) -> Result<SortedTemplate<T>, Error>
+where
+    F: FnMut() -> SortedTemplate<T>,
+{
+    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)),
+    }
+}
 
-        let mut mydst = dst.clone();
-        for part in &remote_path[..remote_path.len() - 1] {
-            mydst.push(part.to_string());
+/// 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>,
+{
+    // write the merged cci to disk
+    for (path, parts) in get_path_parts::<T>(dst, crates_info) {
+        create_parents(&path)?;
+        // read previous rendered cci from storage, append to them
+        let mut template = read_template_or_blank::<_, T::FileFormat>(&mut make_blank, &path)?;
+        for part in parts {
+            template.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)?;
+        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(())
 }
+
+#[cfg(test)]
+mod tests;
diff --git a/src/librustdoc/html/render/write_shared/tests.rs b/src/librustdoc/html/render/write_shared/tests.rs
new file mode 100644
index 00000000000..4d1874b7df5
--- /dev/null
+++ b/src/librustdoc/html/render/write_shared/tests.rs
@@ -0,0 +1,207 @@
+use crate::html::render::ordered_json::{EscapedJson, OrderedJson};
+use crate::html::render::sorted_template::{Html, SortedTemplate};
+use crate::html::render::write_shared::*;
+
+#[test]
+fn hack_external_crate_names() {
+    let path = tempfile::TempDir::new().unwrap();
+    let path = path.path();
+    let crates = hack_get_external_crate_names(&path).unwrap();
+    assert!(crates.is_empty());
+    fs::write(path.join("crates.js"), r#"window.ALL_CRATES = ["a","b","c"];"#).unwrap();
+    let crates = hack_get_external_crate_names(&path).unwrap();
+    assert_eq!(crates, ["a".to_string(), "b".to_string(), "c".to_string()]);
+}
+
+fn but_last_line(s: &str) -> &str {
+    let (before, _) = s.rsplit_once("\n").unwrap();
+    before
+}
+
+#[test]
+fn sources_template() {
+    let mut template = SourcesPart::blank();
+    assert_eq!(
+        but_last_line(&template.to_string()),
+        r"var srcIndex = new Map(JSON.parse('[]'));
+createSrcSidebar();"
+    );
+    template.append(EscapedJson::from(OrderedJson::serialize("u").unwrap()).to_string());
+    assert_eq!(
+        but_last_line(&template.to_string()),
+        r#"var srcIndex = new Map(JSON.parse('["u"]'));
+createSrcSidebar();"#
+    );
+    template.append(EscapedJson::from(OrderedJson::serialize("v").unwrap()).to_string());
+    assert_eq!(
+        but_last_line(&template.to_string()),
+        r#"var srcIndex = new Map(JSON.parse('["u","v"]'));
+createSrcSidebar();"#
+    );
+}
+
+#[test]
+fn sources_parts() {
+    let parts =
+        SearchIndexPart::get(OrderedJson::serialize(["foo", "bar"]).unwrap(), "suffix").unwrap();
+    assert_eq!(&parts.parts[0].0, Path::new("search-indexsuffix.js"));
+    assert_eq!(&parts.parts[0].1.to_string(), r#"["foo","bar"]"#);
+}
+
+#[test]
+fn all_crates_template() {
+    let mut template = AllCratesPart::blank();
+    assert_eq!(but_last_line(&template.to_string()), r"window.ALL_CRATES = [];");
+    template.append(EscapedJson::from(OrderedJson::serialize("b").unwrap()).to_string());
+    assert_eq!(but_last_line(&template.to_string()), r#"window.ALL_CRATES = ["b"];"#);
+    template.append(EscapedJson::from(OrderedJson::serialize("a").unwrap()).to_string());
+    assert_eq!(but_last_line(&template.to_string()), r#"window.ALL_CRATES = ["a","b"];"#);
+}
+
+#[test]
+fn all_crates_parts() {
+    let parts = AllCratesPart::get(OrderedJson::serialize("crate").unwrap()).unwrap();
+    assert_eq!(&parts.parts[0].0, Path::new("crates.js"));
+    assert_eq!(&parts.parts[0].1.to_string(), r#""crate""#);
+}
+
+#[test]
+fn search_index_template() {
+    let mut template = SearchIndexPart::blank();
+    assert_eq!(
+        but_last_line(&template.to_string()),
+        r"var searchIndex = new Map(JSON.parse('[]'));
+if (typeof exports !== 'undefined') exports.searchIndex = searchIndex;
+else if (window.initSearch) window.initSearch(searchIndex);"
+    );
+    template.append(EscapedJson::from(OrderedJson::serialize([1, 2]).unwrap()).to_string());
+    assert_eq!(
+        but_last_line(&template.to_string()),
+        r"var searchIndex = new Map(JSON.parse('[[1,2]]'));
+if (typeof exports !== 'undefined') exports.searchIndex = searchIndex;
+else if (window.initSearch) window.initSearch(searchIndex);"
+    );
+    template.append(EscapedJson::from(OrderedJson::serialize([4, 3]).unwrap()).to_string());
+    assert_eq!(
+        but_last_line(&template.to_string()),
+        r"var searchIndex = new Map(JSON.parse('[[1,2],[4,3]]'));
+if (typeof exports !== 'undefined') exports.searchIndex = searchIndex;
+else if (window.initSearch) window.initSearch(searchIndex);"
+    );
+}
+
+#[test]
+fn crates_index_part() {
+    let external_crates = ["bar".to_string(), "baz".to_string()];
+    let mut parts = CratesIndexPart::get("foo", &external_crates).unwrap();
+    parts.parts.sort_by(|a, b| a.1.to_string().cmp(&b.1.to_string()));
+
+    assert_eq!(&parts.parts[0].0, Path::new("index.html"));
+    assert_eq!(&parts.parts[0].1.to_string(), r#"<li><a href="bar/index.html">bar</a></li>"#);
+
+    assert_eq!(&parts.parts[1].0, Path::new("index.html"));
+    assert_eq!(&parts.parts[1].1.to_string(), r#"<li><a href="baz/index.html">baz</a></li>"#);
+
+    assert_eq!(&parts.parts[2].0, Path::new("index.html"));
+    assert_eq!(&parts.parts[2].1.to_string(), r#"<li><a href="foo/index.html">foo</a></li>"#);
+}
+
+#[test]
+fn trait_alias_template() {
+    let mut template = TraitAliasPart::blank();
+    assert_eq!(
+        but_last_line(&template.to_string()),
+        r#"(function() {
+    var implementors = Object.fromEntries([]);
+    if (window.register_implementors) {
+        window.register_implementors(implementors);
+    } else {
+        window.pending_implementors = implementors;
+    }
+})()"#,
+    );
+    template.append(OrderedJson::serialize(["a"]).unwrap().to_string());
+    assert_eq!(
+        but_last_line(&template.to_string()),
+        r#"(function() {
+    var implementors = Object.fromEntries([["a"]]);
+    if (window.register_implementors) {
+        window.register_implementors(implementors);
+    } else {
+        window.pending_implementors = implementors;
+    }
+})()"#,
+    );
+    template.append(OrderedJson::serialize(["b"]).unwrap().to_string());
+    assert_eq!(
+        but_last_line(&template.to_string()),
+        r#"(function() {
+    var implementors = Object.fromEntries([["a"],["b"]]);
+    if (window.register_implementors) {
+        window.register_implementors(implementors);
+    } else {
+        window.pending_implementors = implementors;
+    }
+})()"#,
+    );
+}
+
+#[test]
+fn type_alias_template() {
+    let mut template = TypeAliasPart::blank();
+    assert_eq!(
+        but_last_line(&template.to_string()),
+        r#"(function() {
+    var type_impls = Object.fromEntries([]);
+    if (window.register_type_impls) {
+        window.register_type_impls(type_impls);
+    } else {
+        window.pending_type_impls = type_impls;
+    }
+})()"#,
+    );
+    template.append(OrderedJson::serialize(["a"]).unwrap().to_string());
+    assert_eq!(
+        but_last_line(&template.to_string()),
+        r#"(function() {
+    var type_impls = Object.fromEntries([["a"]]);
+    if (window.register_type_impls) {
+        window.register_type_impls(type_impls);
+    } else {
+        window.pending_type_impls = type_impls;
+    }
+})()"#,
+    );
+    template.append(OrderedJson::serialize(["b"]).unwrap().to_string());
+    assert_eq!(
+        but_last_line(&template.to_string()),
+        r#"(function() {
+    var type_impls = Object.fromEntries([["a"],["b"]]);
+    if (window.register_type_impls) {
+        window.register_type_impls(type_impls);
+    } else {
+        window.pending_type_impls = type_impls;
+    }
+})()"#,
+    );
+}
+
+#[test]
+fn read_template_test() {
+    let path = tempfile::TempDir::new().unwrap();
+    let path = path.path().join("file.html");
+    let make_blank = || SortedTemplate::<Html>::from_before_after("<div>", "</div>");
+
+    let template = read_template_or_blank(make_blank, &path).unwrap();
+    assert_eq!(but_last_line(&template.to_string()), "<div></div>");
+    fs::write(&path, template.to_string()).unwrap();
+    let mut template = read_template_or_blank(make_blank, &path).unwrap();
+    template.append("<img/>".to_string());
+    fs::write(&path, template.to_string()).unwrap();
+    let mut template = read_template_or_blank(make_blank, &path).unwrap();
+    template.append("<br/>".to_string());
+    fs::write(&path, template.to_string()).unwrap();
+    let template = read_template_or_blank(make_blank, &path).unwrap();
+
+    assert_eq!(but_last_line(&template.to_string()), "<div><br/><img/></div>");
+}