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