1use std::fs;
20use std::path::Path;
21
22use anyhow::{Context, Result};
23
24pub const RUNTIME_IMPORT_URL: &str = "/static/islands-core/islands_core.js";
27
28pub const PATCH_TOKEN: &str = "Object.assign(Object.create(null), import1)";
31
32const NEEDLE: &str = "\"/static/islands-core/islands_core.js\": import1,";
34
35const PATCHED: &str =
37 "\"/static/islands-core/islands_core.js\": Object.assign(Object.create(null), import1),";
38
39pub fn patch_page_js(js_path: &Path) -> Result<bool> {
46 let content =
47 fs::read_to_string(js_path).with_context(|| format!("reading {}", js_path.display()))?;
48 if !content.contains(NEEDLE) {
49 return Ok(false);
50 }
51 let new_content = content.replacen(NEEDLE, PATCHED, 1);
52 fs::write(js_path, new_content)
53 .with_context(|| format!("writing patched {}", js_path.display()))?;
54 Ok(true)
55}
56
57pub fn patch_page_source(source: &str) -> String {
59 if !source.contains(NEEDLE) {
60 return source.to_owned();
61 }
62 source.replacen(NEEDLE, PATCHED, 1)
63}
64
65pub fn assert_patched(js_path: &Path) -> Result<()> {
71 let content =
72 fs::read_to_string(js_path).with_context(|| format!("reading {}", js_path.display()))?;
73 if content.contains(PATCH_TOKEN) {
74 return Ok(());
75 }
76 anyhow::bail!(
77 "V8 namespace patch missing from {} — wasm-bindgen's emitted import shape changed. \
78 Neither the patch token (`{PATCH_TOKEN}`) nor the expected needle (`{NEEDLE}`) is \
79 present. Update islands_build::patch::NEEDLE to match the new wasm-bindgen output.",
80 js_path.display()
81 )
82}
83
84pub fn rewrite_basenames_in_js(source: &str, rename_map: &[(String, String)]) -> String {
89 let mut content = source.to_owned();
90 for (original, hashed) in rename_map {
91 if content.contains(original.as_str()) {
92 content = content.replace(original.as_str(), hashed.as_str());
93 }
94 }
95 for (original, hashed) in rename_map {
96 if !original.contains("islands_core.js") {
97 continue;
98 }
99 let rewritten_key_line = format!(
100 r#""/static/islands-core/{hashed}": Object.assign(Object.create(null), import1)"#
101 );
102 let original_key_line = format!(
103 r#""/static/islands-core/{original}": Object.assign(Object.create(null), import1)"#
104 );
105 content = content.replace(&rewritten_key_line, &original_key_line);
106 }
107 content
108}
109
110pub fn patch_runtime_snippets(js_path: &Path) -> Result<bool> {
120 let content =
121 fs::read_to_string(js_path).with_context(|| format!("reading {}", js_path.display()))?;
122 let patched = wrap_snippet_namespace_imports(&content);
123 if patched == content {
124 return Ok(false);
125 }
126 fs::write(js_path, &patched)
127 .with_context(|| format!("writing patched {}", js_path.display()))?;
128 Ok(true)
129}
130
131pub fn wrap_snippet_namespace_imports(source: &str) -> String {
135 let mut changed = false;
136 let mut lines: Vec<String> = Vec::with_capacity(source.lines().count());
137 for line in source.lines() {
138 match wrap_snippet_import_value(line) {
139 Some(wrapped) => {
140 lines.push(wrapped);
141 changed = true;
142 }
143 None => lines.push(line.to_owned()),
144 }
145 }
146 if !changed {
147 return source.to_owned();
148 }
149 let mut output = lines.join("\n");
150 if source.ends_with('\n') {
151 output.push('\n');
152 }
153 output
154}
155
156fn wrap_snippet_import_value(line: &str) -> Option<String> {
162 if !line.contains("/snippets/") {
163 return None;
164 }
165 let separator = "\": ";
166 let separator_index = line.find(separator)?;
167 let key_through_separator = &line[..separator_index + separator.len()];
168 let value_part = line[separator_index + separator.len()..].trim_end();
169 let identifier = value_part.strip_suffix(',')?;
170 let digits = identifier.strip_prefix("import")?;
173 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
174 return None;
175 }
176 Some(format!(
177 "{key_through_separator}Object.assign(Object.create(null), {identifier}),"
178 ))
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn patches_the_page_core_import_value() {
187 let source = format!(" {NEEDLE}\n");
188 let patched = patch_page_source(&source);
189 assert!(patched.contains(PATCH_TOKEN), "got: {patched}");
190 }
191
192 #[test]
193 fn page_patch_is_idempotent_in_memory() {
194 let once = patch_page_source(&format!(" {NEEDLE}\n"));
195 let twice = patch_page_source(&once);
196 assert_eq!(once, twice);
197 }
198
199 #[test]
200 fn wraps_snippet_importobject_namespace_value() {
201 let source = " \"./snippets/islands-runtime-abc/inline0.js\": import1,\n";
202 let output = wrap_snippet_namespace_imports(source);
203 assert!(
204 output.contains(
205 "\"./snippets/islands-runtime-abc/inline0.js\": Object.assign(Object.create(null), import1),"
206 ),
207 "snippet importObject value must be namespace-wrapped; got: {output}"
208 );
209 }
210
211 #[test]
212 fn leaves_import_statement_and_non_snippet_lines_untouched() {
213 let source = concat!(
214 "import * as import1 from \"./snippets/islands-runtime-abc/inline0.js\";\n",
215 " \"__wbindgen_placeholder__\": import1,\n",
216 );
217 assert_eq!(wrap_snippet_namespace_imports(source), source);
218 }
219
220 #[test]
221 fn snippet_wrap_is_idempotent() {
222 let source = " \"./snippets/x/inline0.js\": import2,\n";
223 let once = wrap_snippet_namespace_imports(source);
224 let twice = wrap_snippet_namespace_imports(&once);
225 assert_eq!(once, twice, "re-running the wrap must be a no-op");
226 }
227
228 #[test]
229 fn page_patch_token_survives_basename_rewrite() {
230 let patched = patch_page_source(&format!(" {NEEDLE}\n"));
233 let rename_map = vec![(
234 "islands_core.js".to_owned(),
235 "islands_core.deadbeef.js".to_owned(),
236 )];
237 let after = rewrite_basenames_in_js(&patched, &rename_map);
238 assert!(after.contains(PATCH_TOKEN), "patch token lost: {after}");
239 assert!(
240 after.contains("/static/islands-core/islands_core.js\":"),
241 "runtime importObject key must be restored unhashed: {after}"
242 );
243 }
244
245 #[test]
246 fn snippet_key_stays_unhashed_through_rewrite() {
247 let wrapped = wrap_snippet_namespace_imports(" \"./snippets/r/inline0.js\": import1,\n");
248 let rename_map = vec![(
249 "islands_core.js".to_owned(),
250 "islands_core.deadbeef.js".to_owned(),
251 )];
252 let after = rewrite_basenames_in_js(&wrapped, &rename_map);
253 assert!(
254 after.contains(
255 "\"./snippets/r/inline0.js\": Object.assign(Object.create(null), import1),"
256 ),
257 "snippet key must stay unhashed and the wrap must survive; got: {after}"
258 );
259 }
260}