1#![doc = include_str!("../Documentation.md")]
2#![forbid(unsafe_code)]
3
4use kcode_k1_transaction_id::TxId;
5use kcode_k1_web_bootstrap_archive::{Archive, Package};
6use kcode_k1_web_bootstrap_preflight::inspect;
7use kcode_k1_web_coding::{CheckOutcome, K1WebCoding, WebCodingConfig, WebCodingRevisions};
8use kcode_k1_web_package::{
9 AuthorityId, DependencySelector, SourceFile, SourcePackage, WebFamily, WebId,
10};
11use kcode_k1_web_podman::WebPodmanConfig;
12use kcode_k1_web_projection::{K1WebProjection, PublishStatus};
13use semver::Version;
14use serde::Serialize;
15use sha2::{Digest, Sha256};
16use std::collections::BTreeSet;
17use std::fs::{File, OpenOptions};
18use std::io::Write;
19use std::path::{Path, PathBuf};
20use std::sync::Arc;
21
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct ImporterRevisions {
24 pub boot: String,
25 pub schema: String,
26 pub route: String,
27 pub harness: String,
28 pub check_policy: String,
29}
30
31#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct ImportedPackage {
33 name: String,
34 version: Version,
35 source_sha256: [u8; 32],
36}
37
38impl ImportedPackage {
39 pub fn name(&self) -> &str {
40 &self.name
41 }
42
43 pub fn version(&self) -> &Version {
44 &self.version
45 }
46
47 pub const fn source_sha256(&self) -> [u8; 32] {
48 self.source_sha256
49 }
50}
51
52#[derive(Clone)]
53pub struct WebBootstrapImporter {
54 inner: Arc<Inner>,
55}
56
57struct Inner {
58 cache_root: PathBuf,
59 projection_root: PathBuf,
60 revisions: ImporterRevisions,
61 podman: WebPodmanConfig,
62 projection: Arc<K1WebProjection>,
63}
64
65impl WebBootstrapImporter {
66 pub fn new(
67 cache_root: impl Into<PathBuf>,
68 projection_root: impl Into<PathBuf>,
69 revisions: ImporterRevisions,
70 podman: WebPodmanConfig,
71 projection: Arc<K1WebProjection>,
72 ) -> Result<Self, String> {
73 let cache_root = cache_root.into();
74 let projection_root = projection_root.into();
75 require_directory(&cache_root, "Web bootstrap cache")?;
76 require_directory(&projection_root, "Web projection")?;
77 if [
78 &revisions.boot,
79 &revisions.schema,
80 &revisions.route,
81 &revisions.harness,
82 &revisions.check_policy,
83 ]
84 .iter()
85 .any(|value| value.is_empty())
86 {
87 return Err("Web bootstrap importer revisions must be nonempty".to_owned());
88 }
89 Ok(Self {
90 inner: Arc::new(Inner {
91 cache_root,
92 projection_root,
93 revisions,
94 podman,
95 projection,
96 }),
97 })
98 }
99
100 pub fn import_all(
101 &self,
102 archive: &Archive,
103 authority: [u8; 12],
104 user: [u8; 12],
105 log_path: impl AsRef<Path>,
106 ) -> Result<Vec<ImportedPackage>, String> {
107 let log_path = log_path.as_ref();
108 let mut log = ImportLog::append(log_path)?;
109 let plan = inspect(archive);
110 log.line(&format!(
111 "IMPORT START packages={} warnings={}",
112 plan.packages().len(),
113 plan.warnings().len()
114 ))?;
115
116 let authority = AuthorityId::new(TxId::from_bytes(authority));
117 let mut prepared = std::iter::repeat_with(|| None)
118 .take(archive.packages().len())
119 .collect::<Vec<Option<PreparedPackage>>>();
120 let mut failures = 0_usize;
121
122 for &index in plan.order() {
123 let package = &archive.packages()[index];
124 let label = package_label(package);
125 match prepare_package(package, authority) {
126 Ok(item) => prepared[index] = Some(item),
127 Err(error) => {
128 log.line(&format!("ERROR prepare {label}: {error}"))?;
129 failures += 1;
130 }
131 }
132 }
133
134 let mut coding = self.open_diagnostic_coding(user, &mut log)?;
135 let authorize = |family: &WebFamily| Ok(family.authority() == authority);
136 let mut imported = Vec::with_capacity(archive.packages().len() - failures);
137
138 for &index in plan.order() {
139 let Some(item) = prepared[index].as_ref() else {
140 continue;
141 };
142 let label = coordinate_label(&item.coordinate);
143 log.line(&format!("ATTEMPT {label}"))?;
144
145 match coding.as_mut() {
146 Some(coding) => {
147 log_chromium_check(coding, item, &authorize, &label, &mut log)?;
148 }
149 None => {
150 log.line(&format!(
151 "WARN CHECK {label}: Chromium diagnostics unavailable; proceeding with trusted immutable publication"
152 ))?;
153 }
154 }
155
156 match self.publish_one(item) {
157 Ok(()) => {
158 log.line(&format!("IMPORTED {label}"))?;
159 imported.push(ImportedPackage {
160 name: item.coordinate.name.clone(),
161 version: item.coordinate.version.clone(),
162 source_sha256: item.source_sha256,
163 });
164 }
165 Err(error) => {
166 log.line(&format!("ERROR import {label}: {error}"))?;
167 failures += 1;
168 }
169 }
170 }
171
172 if failures != 0 {
173 log.line(&format!(
174 "FAILURE imported={} remaining={failures}",
175 imported.len()
176 ))?;
177 return Err(format!(
178 "Web bootstrap left {failures} package(s) concretely unimported; inspect {}",
179 log_path.display()
180 ));
181 }
182
183 log.line(&format!("SUCCESS packages={}", imported.len()))?;
184 Ok(imported)
185 }
186
187 fn open_diagnostic_coding(
188 &self,
189 user: [u8; 12],
190 log: &mut ImportLog,
191 ) -> Result<Option<K1WebCoding>, String> {
192 let revisions = &self.inner.revisions;
193 let config = match WebCodingConfig::new(
194 WebCodingRevisions {
195 boot: revisions.boot.clone(),
196 schema: revisions.schema.clone(),
197 route: revisions.route.clone(),
198 harness: revisions.harness.clone(),
199 check_policy: revisions.check_policy.clone(),
200 },
201 self.inner.projection_root.clone(),
202 self.inner.podman.clone(),
203 ) {
204 Ok(config) => config,
205 Err(error) => {
206 log.line(&format!(
207 "WARN Chromium diagnostic configuration unavailable: {error:#?}"
208 ))?;
209 return Ok(None);
210 }
211 };
212
213 match K1WebCoding::open(
214 &self.inner.cache_root,
215 TxId::from_bytes(user),
216 config,
217 Arc::clone(&self.inner.projection),
218 ) {
219 Ok(coding) => Ok(Some(coding)),
220 Err(error) => {
221 log.line(&format!(
222 "WARN Chromium diagnostic runner unavailable: {error:#?}"
223 ))?;
224 Ok(None)
225 }
226 }
227 }
228
229 fn publish_one(&self, item: &PreparedPackage) -> Result<(), String> {
230 let label = coordinate_label(&item.coordinate);
231 match self
232 .inner
233 .projection
234 .load(item.source.id())
235 .map_err(|error| format!("inspect existing publication: {error}"))?
236 {
237 Some(existing) if existing.as_ref() == &item.source => return Ok(()),
238 Some(_) => {
239 return Err(format!(
240 "published Web package {label} has conflicting source bytes"
241 ));
242 }
243 None => {}
244 }
245
246 match self.inner.projection.publish(&item.source) {
247 Ok(outcome) if outcome.status != PublishStatus::Conflict => Ok(()),
248 Ok(_) => Err(format!(
249 "published Web package {label} reported an immutable conflict"
250 )),
251 Err(error) => match self.inner.projection.load(item.source.id()) {
252 Ok(Some(existing)) if existing.as_ref() == &item.source => Ok(()),
253 Ok(Some(_)) => Err(format!(
254 "publish projection reported {error:?}; canonical Web package {label} has conflicting source bytes"
255 )),
256 Ok(None) => Err(format!(
257 "publish projection reported {error:?}; no canonical publication exists"
258 )),
259 Err(reconcile) => Err(format!(
260 "publish projection reported {error:?}; inspect canonical publication: {reconcile}"
261 )),
262 },
263 }
264 }
265}
266
267fn log_chromium_check(
268 coding: &mut K1WebCoding,
269 item: &PreparedPackage,
270 authorize: &impl Fn(&WebFamily) -> Result<bool, String>,
271 label: &str,
272 log: &mut ImportLog,
273) -> Result<(), String> {
274 if let Err(error) = coding.write(&item.source, authorize) {
275 return log.line(&format!(
276 "WARN CHECK {label}: diagnostic staging unavailable: {error:#?}; proceeding with trusted immutable publication"
277 ));
278 }
279
280 match coding.check_fresh(item.source.id(), authorize) {
281 Ok(CheckOutcome::Checked(execution)) => {
282 log.line(&format!("CHECK {label}: {execution:#?}"))
283 }
284 Ok(CheckOutcome::Reused) => log.line(&format!(
285 "WARN CHECK {label}: fresh Chromium check was unexpectedly reused; proceeding with trusted immutable publication"
286 )),
287 Err(error) => log.line(&format!(
288 "WARN CHECK {label}: {error:#?}; proceeding with trusted immutable publication"
289 )),
290 }
291}
292
293pub fn write_preflight_log(archive: &Archive, log_path: impl AsRef<Path>) -> Result<(), String> {
294 let log_path = log_path.as_ref();
295 let plan = inspect(archive);
296 let mut log = ImportLog::replace(log_path)?;
297 log.line(&format!(
298 "START Web bootstrap preflight packages={}",
299 plan.packages().len()
300 ))?;
301 for warning in plan.warnings() {
302 log.line(warning)?;
303 }
304 log.line(&format!(
305 "PREFLIGHT COMPLETE packages={} warnings={}",
306 plan.packages().len(),
307 plan.warnings().len()
308 ))
309}
310
311struct ImportLog(File);
312
313impl ImportLog {
314 fn replace(path: &Path) -> Result<Self, String> {
315 OpenOptions::new()
316 .create(true)
317 .truncate(true)
318 .write(true)
319 .open(path)
320 .map(Self)
321 .map_err(|error| {
322 format!(
323 "initialize Web bootstrap import log {}: {error}",
324 path.display()
325 )
326 })
327 }
328
329 fn append(path: &Path) -> Result<Self, String> {
330 OpenOptions::new()
331 .create(true)
332 .append(true)
333 .open(path)
334 .map(Self)
335 .map_err(|error| format!("open Web bootstrap import log {}: {error}", path.display()))
336 }
337
338 fn line(&mut self, line: &str) -> Result<(), String> {
339 writeln!(self.0, "{line}")
340 .map_err(|error| format!("write Web bootstrap import log: {error}"))?;
341 self.0
342 .flush()
343 .map_err(|error| format!("flush Web bootstrap import log: {error}"))
344 }
345}
346
347#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
348struct Coordinate {
349 name: String,
350 version: Version,
351}
352
353#[derive(Debug)]
354struct PreparedPackage {
355 coordinate: Coordinate,
356 source_sha256: [u8; 32],
357 source: SourcePackage,
358}
359
360#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
361struct GeneratedDependency {
362 authority: String,
363 name: String,
364 selector: String,
365}
366
367#[derive(Serialize)]
368struct Header<'a> {
369 dependencies: &'a [GeneratedDependency],
370}
371
372#[derive(Serialize)]
373struct Manifest<'a> {
374 name: &'a str,
375 version: String,
376 entry: &'a str,
377 tests: &'a str,
378 dependencies: &'a [GeneratedDependency],
379}
380
381fn prepare_package(package: &Package, authority: AuthorityId) -> Result<PreparedPackage, String> {
382 let coordinate = Coordinate {
383 name: package.name().to_owned(),
384 version: package.version().clone(),
385 };
386 let family = WebFamily::new(authority, coordinate.name.clone()).map_err(|error| {
387 format!(
388 "assign Web package {} authority: {error}",
389 coordinate_label(&coordinate)
390 )
391 })?;
392 let id = WebId::new(family, coordinate.version.clone()).map_err(|error| {
393 format!(
394 "assign Web package {} version: {error}",
395 coordinate_label(&coordinate)
396 )
397 })?;
398 let authority_text = authority.to_string();
399 let (dependencies, rewrites) = usable_dependencies(package, authority, &coordinate);
400
401 let original_documentation = package
402 .files()
403 .iter()
404 .find(|file| file.path() == "Documentation.md")
405 .ok_or_else(|| {
406 format!(
407 "Web bootstrap package {} lacks Documentation.md",
408 coordinate_label(&coordinate)
409 )
410 })?
411 .bytes();
412
413 let header = serde_json::to_string(&Header {
414 dependencies: &dependencies,
415 })
416 .map_err(|error| {
417 format!(
418 "encode Web dependencies for {}: {error}",
419 coordinate_label(&coordinate)
420 )
421 })?;
422 let mut documentation = format!("<!-- k1-web/v1\n{header}\n-->\n").into_bytes();
423 documentation.extend_from_slice(original_documentation);
424
425 let manifest = serde_json::to_vec(&Manifest {
426 name: package.name(),
427 version: package.version().to_string(),
428 entry: package.entry(),
429 tests: package.tests(),
430 dependencies: &dependencies,
431 })
432 .map_err(|error| {
433 format!(
434 "encode Web manifest for {}: {error}",
435 coordinate_label(&coordinate)
436 )
437 })?;
438
439 let mut files = Vec::with_capacity(package.files().len() + 1);
440 for file in package.files() {
441 if matches!(file.path(), "Documentation.md" | "k1-web.json") {
442 continue;
443 }
444 files.push(SourceFile::new(
445 file.path(),
446 rewrite_file(file.path(), file.bytes(), &authority_text, &rewrites),
447 ));
448 }
449 files.push(SourceFile::new("Documentation.md", documentation));
450 files.push(SourceFile::new("k1-web.json", manifest));
451
452 let source = SourcePackage::new(id, files).map_err(|error| {
453 format!(
454 "construct Web bootstrap package {}: {error}",
455 coordinate_label(&coordinate)
456 )
457 })?;
458 Ok(PreparedPackage {
459 coordinate,
460 source_sha256: source_digest(&source),
461 source,
462 })
463}
464
465fn usable_dependencies(
466 package: &Package,
467 authority: AuthorityId,
468 coordinate: &Coordinate,
469) -> (Vec<GeneratedDependency>, BTreeSet<(String, String)>) {
470 let authority_text = authority.to_string();
471 let mut dependencies = BTreeSet::new();
472 let mut rewrites = BTreeSet::new();
473
474 for dependency in package.dependencies() {
475 if WebFamily::new(authority, dependency.name().to_owned()).is_err() {
476 continue;
477 }
478 let Ok(selector) = DependencySelector::parse(dependency.selector()) else {
479 continue;
480 };
481 rewrites.insert((
482 dependency.name().to_owned(),
483 dependency.selector().to_owned(),
484 ));
485 if dependency.name() == coordinate.name && selector.matches(&coordinate.version) {
486 continue;
487 }
488 dependencies.insert(GeneratedDependency {
489 authority: authority_text.clone(),
490 name: dependency.name().to_owned(),
491 selector: dependency.selector().to_owned(),
492 });
493 }
494
495 (dependencies.into_iter().collect(), rewrites)
496}
497
498fn rewrite_file(
499 path: &str,
500 bytes: &[u8],
501 authority: &str,
502 declarations: &BTreeSet<(String, String)>,
503) -> Vec<u8> {
504 if !matches!(extension(path), "js" | "mjs" | "html" | "css") {
505 return bytes.to_vec();
506 }
507 let Ok(source) = std::str::from_utf8(bytes) else {
508 return bytes.to_vec();
509 };
510
511 let mut rewritten = source.to_owned();
512 for (name, selector) in declarations {
513 let legacy = format!("/module/{name}/v{selector}/");
514 let route_selector = if selector == "*" {
515 "*".to_owned()
516 } else {
517 format!("v{selector}")
518 };
519 let current = format!("/{authority}/{name}/{route_selector}/");
520 rewritten = rewritten.replace(&legacy, ¤t);
521 }
522 rewritten.into_bytes()
523}
524
525fn extension(path: &str) -> &str {
526 path.rsplit_once('.').map_or("", |(_, extension)| extension)
527}
528
529fn source_digest(source: &SourcePackage) -> [u8; 32] {
530 let mut hash = Sha256::new();
531 for file in source.files() {
532 hash.update((file.path().len() as u64).to_le_bytes());
533 hash.update(file.path().as_bytes());
534 hash.update((file.bytes().len() as u64).to_le_bytes());
535 hash.update(file.bytes());
536 }
537 hash.finalize().into()
538}
539
540fn package_label(package: &Package) -> String {
541 format!("{} {}", package.name(), package.version())
542}
543
544fn coordinate_label(coordinate: &Coordinate) -> String {
545 format!("{} {}", coordinate.name, coordinate.version)
546}
547
548fn require_directory(path: &Path, label: &str) -> Result<(), String> {
549 let metadata = std::fs::symlink_metadata(path)
550 .map_err(|error| format!("inspect {label} {}: {error}", path.display()))?;
551 if metadata.is_dir() && !metadata.file_type().is_symlink() {
552 Ok(())
553 } else {
554 Err(format!("{label} is not an ordinary directory"))
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561 use kcode_k1_web_bootstrap_archive::read;
562 use serde_json::Value;
563 use std::fs::File;
564 use std::io::Write;
565 use zip::write::SimpleFileOptions;
566
567 fn archive(root: &Path, name: &str, manifest: &str, entries: &[(&str, &[u8])]) -> Archive {
568 let path = root.join(name);
569 let file = File::create(&path).unwrap();
570 let mut zip = zip::ZipWriter::new(file);
571 zip.start_file("manifest.json", SimpleFileOptions::default())
572 .unwrap();
573 zip.write_all(manifest.as_bytes()).unwrap();
574 for (path, bytes) in entries {
575 zip.start_file(*path, SimpleFileOptions::default()).unwrap();
576 zip.write_all(bytes).unwrap();
577 }
578 zip.finish().unwrap();
579 read(&path).unwrap().archive
580 }
581
582 fn file<'a>(source: &'a SourcePackage, path: &str) -> &'a [u8] {
583 source
584 .files()
585 .iter()
586 .find(|file| file.path() == path)
587 .unwrap()
588 .bytes()
589 }
590
591 fn prepare_all(archive: &Archive, authority: AuthorityId) -> Vec<PreparedPackage> {
592 let plan = inspect(archive);
593 plan.order()
594 .iter()
595 .map(|index| prepare_package(&archive.packages()[*index], authority).unwrap())
596 .collect()
597 }
598
599 #[test]
600 fn prepares_complete_trees_current_routes_and_exact_metadata() {
601 let root = tempfile::tempdir().unwrap();
602 let manifest = r#"{"schema":1,"packages":[{"name":"dep","version":"1.5.0","entry":"index.js","tests":"tests.js","dependencies":[]},{"name":"dep","version":"2.1.0","entry":"index.js","tests":"tests.js","dependencies":[]},{"name":"bridge","version":"1.0.0","entry":"index.js","tests":"tests.js","dependencies":[{"name":"dep","selector":"2"}]},{"name":"app","version":"1.0.0","entry":"src/index.js","tests":"tests.js","dependencies":[{"name":"app","selector":"1"},{"name":"dep","selector":"1"}]}]}"#;
603 let entries: Vec<(&str, &[u8])> = vec![
604 (
605 "packages/dep/1.5.0/Documentation.md",
606 b"dep one docs\r\n",
607 ),
608 (
609 "packages/dep/1.5.0/index.js",
610 b"export const version=1;\n",
611 ),
612 (
613 "packages/dep/1.5.0/tests.js",
614 b"export function runTests(){}\n",
615 ),
616 (
617 "packages/dep/1.5.0/assets/deep/image.bin",
618 &[0, 1, 255],
619 ),
620 (
621 "packages/dep/2.1.0/Documentation.md",
622 b"dep two docs\n",
623 ),
624 (
625 "packages/dep/2.1.0/index.js",
626 b"export const version=2;\n",
627 ),
628 (
629 "packages/dep/2.1.0/tests.js",
630 b"export function runTests(){}\n",
631 ),
632 (
633 "packages/bridge/1.0.0/Documentation.md",
634 b"bridge docs\n",
635 ),
636 (
637 "packages/bridge/1.0.0/index.js",
638 b"import '/module/dep/v2/index.js';\nexport {};\n",
639 ),
640 (
641 "packages/bridge/1.0.0/tests.js",
642 b"export function runTests(){}\n",
643 ),
644 (
645 "packages/app/1.0.0/Documentation.md",
646 b"app docs\r\n",
647 ),
648 (
649 "packages/app/1.0.0/src/index.js",
650 b"import '/module/dep/v1/index.js';\nexport const self='/module/app/v1/src/index.js';\n",
651 ),
652 (
653 "packages/app/1.0.0/tests.js",
654 b"export function runTests(){}\n",
655 ),
656 (
657 "packages/app/1.0.0/styles/main.css",
658 b"@import '/module/dep/v1/style.css';\n",
659 ),
660 ];
661 let archive = archive(root.path(), "complete.zip", manifest, &entries);
662 let authority = AuthorityId::new(TxId::from_bytes([1; 12]));
663 let authority_text = authority.to_string();
664 let prepared = prepare_all(&archive, authority);
665
666 let dep_one = prepared
667 .iter()
668 .find(|item| {
669 item.coordinate.name == "dep" && item.coordinate.version == Version::new(1, 5, 0)
670 })
671 .unwrap();
672 assert_eq!(file(&dep_one.source, "assets/deep/image.bin"), [0, 1, 255]);
673
674 let app = prepared
675 .iter()
676 .find(|item| item.coordinate.name == "app")
677 .unwrap();
678 assert_eq!(app.source.entry(), "src/index.js");
679 assert_eq!(app.source.tests(), "tests.js");
680 assert_eq!(app.source.dependencies().len(), 1);
681 assert_eq!(app.source.dependencies()[0].name(), "dep");
682 let expected_documentation = format!(
683 "<!-- k1-web/v1\n{{\"dependencies\":[{{\"authority\":\"{authority_text}\",\"name\":\"dep\",\"selector\":\"1\"}}]}}\n-->\napp docs\r\n"
684 );
685 assert_eq!(
686 file(&app.source, "Documentation.md"),
687 expected_documentation.as_bytes()
688 );
689
690 let generated: Value = serde_json::from_slice(file(&app.source, "k1-web.json")).unwrap();
691 assert_eq!(generated["name"], "app");
692 assert_eq!(generated["version"], "1.0.0");
693 assert_eq!(generated["entry"], "src/index.js");
694 assert_eq!(generated["tests"], "tests.js");
695 assert_eq!(generated["dependencies"].as_array().unwrap().len(), 1);
696
697 let code = std::str::from_utf8(file(&app.source, "src/index.js")).unwrap();
698 assert!(code.contains(&format!("/{authority_text}/dep/v1/index.js")));
699 assert!(code.contains(&format!("/{authority_text}/app/v1/src/index.js")));
700 assert!(!code.contains("/module/"));
701
702 let style = std::str::from_utf8(file(&app.source, "styles/main.css")).unwrap();
703 assert!(style.contains(&format!("/{authority_text}/dep/v1/style.css")));
704 }
705
706 #[test]
707 fn warnings_do_not_prevent_preparation() {
708 let root = tempfile::tempdir().unwrap();
709 let manifest = r#"{"schema":1,"packages":[{"name":"a","version":"1.0.0","entry":"index.js","tests":"tests.js","dependencies":[{"name":"missing","selector":"1"}]},{"name":"b","version":"1.0.0","entry":"index.js","tests":"tests.js","dependencies":[{"name":"a","selector":"1"},{"name":"bad name","selector":"not a selector"}]},{"name":"c","version":"1.0.0","entry":"index.js","tests":"tests.js","dependencies":[{"name":"b","selector":"1"},{"name":"c","selector":"1"}]}]}"#;
710 let entries: Vec<(&str, &[u8])> = vec![
711 ("packages/a/1.0.0/Documentation.md", b"a docs\n"),
712 (
713 "packages/a/1.0.0/index.js",
714 b"import '/module/missing/v1/index.js';\n",
715 ),
716 (
717 "packages/a/1.0.0/tests.js",
718 b"export function runTests(){}\n",
719 ),
720 ("packages/b/1.0.0/Documentation.md", b"b docs\n"),
721 (
722 "packages/b/1.0.0/index.js",
723 b"import '/module/a/v1/index.js';\n",
724 ),
725 (
726 "packages/b/1.0.0/tests.js",
727 b"export function runTests(){}\n",
728 ),
729 ("packages/c/1.0.0/Documentation.md", b"c docs\n"),
730 (
731 "packages/c/1.0.0/index.js",
732 b"import '/module/b/v1/index.js';\n",
733 ),
734 (
735 "packages/c/1.0.0/tests.js",
736 b"export function runTests(){}\n",
737 ),
738 ];
739 let archive = archive(root.path(), "warnings.zip", manifest, &entries);
740 let plan = inspect(&archive);
741 assert!(!plan.warnings().is_empty());
742
743 let authority = AuthorityId::new(TxId::from_bytes([2; 12]));
744 let prepared = prepare_all(&archive, authority);
745 assert_eq!(prepared.len(), 3);
746
747 let a = prepared
748 .iter()
749 .find(|item| item.coordinate.name == "a")
750 .unwrap();
751 assert_eq!(a.source.dependencies().len(), 1);
752 assert_eq!(a.source.dependencies()[0].name(), "missing");
753
754 let b = prepared
755 .iter()
756 .find(|item| item.coordinate.name == "b")
757 .unwrap();
758 assert_eq!(b.source.dependencies().len(), 1);
759 assert_eq!(b.source.dependencies()[0].name(), "a");
760
761 let c = prepared
762 .iter()
763 .find(|item| item.coordinate.name == "c")
764 .unwrap();
765 assert_eq!(c.source.dependencies().len(), 1);
766 assert_eq!(c.source.dependencies()[0].name(), "b");
767 }
768
769 #[test]
770 fn preflight_log_contains_all_warnings_and_is_replaced() {
771 let root = tempfile::tempdir().unwrap();
772 let manifest = r#"{"schema":1,"packages":[{"name":"a","version":"1.0.0","entry":"index.js","tests":"tests.js","dependencies":[{"name":"missing-one","selector":"1"},{"name":"missing-two","selector":"2"}]}]}"#;
773 let entries: Vec<(&str, &[u8])> = vec![
774 ("packages/a/1.0.0/Documentation.md", b"a docs\n"),
775 ("packages/a/1.0.0/index.js", b"export {};\n"),
776 (
777 "packages/a/1.0.0/tests.js",
778 b"export function runTests(){}\n",
779 ),
780 ];
781 let archive = archive(root.path(), "log.zip", manifest, &entries);
782 let log = root.path().join("import.log");
783 std::fs::write(&log, "old contents\n").unwrap();
784
785 write_preflight_log(&archive, &log).unwrap();
786 let text = std::fs::read_to_string(log).unwrap();
787 assert!(!text.contains("old contents"));
788 assert_eq!(text.matches("WARN unresolved dependency").count(), 2);
789 assert!(text.contains("PREFLIGHT COMPLETE"));
790 }
791}