1#![allow(clippy::too_many_lines)]
8
9use std::collections::{BTreeMap, BTreeSet};
10use std::fmt;
11use std::fmt::Write as _;
12use std::path::{Component, PathBuf};
13
14use serde::Serialize;
15
16use crate::platform::{
17 validate_workspace_configuration_v1, workspace_configuration_fingerprint_v1, Digest,
18 WorkspaceConfiguration,
19};
20use crate::semantic_package::SemanticPackageResolutionTable;
21use crate::semantic_package_runtime::SemanticPackageRuntimeModuleTable;
22use crate::{
23 build_application_semantic_model_for_unit_with_packages, build_production_reports,
24 build_production_runtime_artifact, build_resume_chunk_graph, build_resume_manifest,
25 build_runtime_component_artifact, build_runtime_computed_artifact,
26 build_runtime_context_artifact, build_runtime_effect_artifact, build_runtime_forms_artifact,
27 build_runtime_opaque_artifact_with_modules, build_runtime_resource_artifact_with_modules,
28 build_template_manifest_from_asm, embed_opaque_runtime_artifact, emit_production_modules,
29 extract_production_chunk_graph, generate_ordinary_instance_html_for_component,
30 generate_runtime_stub, generate_standalone_page_with_resume_runtime,
31 generate_standalone_page_with_resume_runtime_and_resources, lower_components_to_ir,
32 optimization_report_json, optimize_context_ir, optimize_effect_ir,
33 production_runtime_artifact_json, resume_manifest_json, runtime_component_artifact_json,
34 runtime_computed_artifact_json, runtime_context_artifact_json, runtime_cost_report_json,
35 runtime_effect_artifact_json, runtime_forms_artifact_json, runtime_opaque_artifact_json,
36 runtime_resource_artifact_json, template_manifest_json, validate_runtime_opaque_artifact,
37 validate_runtime_resource_artifact, CompilationUnit, ConstantFoldingPass,
38 ExecutableProgramFingerprint, ImmutableAsmPass, ProductionReportInputs,
39 ProductionRootChunkInput, SemanticId, SharedChunkCandidatePlan,
40};
41
42pub const APPLICATION_PUBLICATION_MANIFEST_SCHEMA_VERSION: u32 = 1;
43pub const APPLICATION_PUBLICATION_COMPILER_CONTRACT_V1: &str = "presolve-application-publication:1";
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum ApplicationPublicationProfileV1 {
47 Development,
48 Production,
49}
50
51impl ApplicationPublicationProfileV1 {
52 #[must_use]
53 pub const fn as_str(self) -> &'static str {
54 match self {
55 Self::Development => "development",
56 Self::Production => "production",
57 }
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ApplicationPublicationSourceV1 {
63 pub logical_path: PathBuf,
64 pub source: String,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct ApplicationPublicationRequestV1 {
69 pub configuration: WorkspaceConfiguration,
70 pub sources: Vec<ApplicationPublicationSourceV1>,
73 pub entry_path: PathBuf,
74 pub package_contracts: SemanticPackageResolutionTable,
75 pub package_runtime_modules: SemanticPackageRuntimeModuleTable,
76 pub profile: ApplicationPublicationProfileV1,
77 pub output_root: PathBuf,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct ValidatedApplicationPublicationRequestV1 {
82 pub request: ApplicationPublicationRequestV1,
83 pub unit: CompilationUnit,
84 pub entry_component: SemanticId,
88 pub render_root_component: SemanticId,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct ApplicationPublicationRequestErrorV1 {
95 pub code: &'static str,
96 pub message: String,
97}
98
99impl fmt::Display for ApplicationPublicationRequestErrorV1 {
100 fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
101 write!(output, "{}: {}", self.code, self.message)
102 }
103}
104
105impl std::error::Error for ApplicationPublicationRequestErrorV1 {}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct ApplicationPublicationErrorV1 {
109 pub code: &'static str,
110 pub message: String,
111}
112
113impl fmt::Display for ApplicationPublicationErrorV1 {
114 fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
115 write!(output, "{}: {}", self.code, self.message)
116 }
117}
118
119impl std::error::Error for ApplicationPublicationErrorV1 {}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
122pub struct ApplicationPublicationArtifactV1 {
123 pub path: String,
124 pub digest: String,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
128pub struct ApplicationPublicationManifestV1 {
129 pub schema_version: u32,
130 pub compiler_contract: String,
131 pub workspace_snapshot_id: String,
132 pub entry_component_id: String,
133 pub profile: String,
134 pub artifacts: Vec<ApplicationPublicationArtifactV1>,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct ApplicationPublicationProductV1 {
139 pub manifest: ApplicationPublicationManifestV1,
140 pub artifacts: BTreeMap<PathBuf, Vec<u8>>,
143}
144
145#[must_use]
146pub fn application_publication_manifest_json_v1(
152 manifest: &ApplicationPublicationManifestV1,
153) -> String {
154 serde_json::to_string_pretty(manifest)
155 .expect("application-publication manifest is serializable")
156 + "\n"
157}
158
159pub fn validate_application_publication_request_v1(
166 request: ApplicationPublicationRequestV1,
167) -> Result<ValidatedApplicationPublicationRequestV1, ApplicationPublicationRequestErrorV1> {
168 if let Err(error) = validate_workspace_configuration_v1(&request.configuration) {
169 return Err(ApplicationPublicationRequestErrorV1 {
170 code: "PSAPP1007_INVALID_WORKSPACE_CONFIGURATION",
171 message: error.message,
172 });
173 }
174 if request.sources.is_empty() {
175 return Err(ApplicationPublicationRequestErrorV1 {
176 code: "PSAPP1001_EMPTY_SOURCE_SET",
177 message: "application publication requires at least one explicit source".into(),
178 });
179 }
180 if request.entry_path.as_os_str().is_empty()
181 || request.entry_path.is_absolute()
182 || request.entry_path.components().any(|component| {
183 matches!(
184 component,
185 Component::ParentDir | Component::RootDir | Component::Prefix(_)
186 )
187 })
188 {
189 return Err(ApplicationPublicationRequestErrorV1 {
190 code: "PSAPP1002_INVALID_ENTRY_PATH",
191 message: "application entry path must be a non-empty relative logical path".into(),
192 });
193 }
194 let paths = request
195 .sources
196 .iter()
197 .map(|source| source.logical_path.clone())
198 .collect::<BTreeSet<_>>();
199 if paths.len() != request.sources.len() {
200 return Err(ApplicationPublicationRequestErrorV1 {
201 code: "PSAPP1003_DUPLICATE_LOGICAL_SOURCE",
202 message: "application publication source logical paths must be unique".into(),
203 });
204 }
205 if !paths.contains(&request.entry_path) {
206 return Err(ApplicationPublicationRequestErrorV1 {
207 code: "PSAPP1004_ENTRY_NOT_IN_SOURCE_SET",
208 message: "application entry path must name one explicit source".into(),
209 });
210 }
211 let unit = CompilationUnit::parse_sources(
212 request
213 .sources
214 .iter()
215 .map(|source| (&source.logical_path, source.source.as_str())),
216 );
217 let model =
218 build_application_semantic_model_for_unit_with_packages(&unit, &request.package_contracts);
219 let entries = model
220 .components
221 .iter()
222 .filter(|component| {
223 component.module_path == request.entry_path
224 && component.element_name.is_some()
225 && component.render.is_some()
226 })
227 .map(|component| component.id.clone())
228 .collect::<Vec<_>>();
229 let [entry_component] = entries.as_slice() else {
230 return Err(ApplicationPublicationRequestErrorV1 {
231 code: if entries.is_empty() {
232 "PSAPP1005_ENTRY_APPLICATION_ROOT_MISSING"
233 } else {
234 "PSAPP1006_ENTRY_APPLICATION_ROOT_AMBIGUOUS"
235 },
236 message:
237 "application entry source must declare exactly one supported rendered component"
238 .into(),
239 });
240 };
241 Ok(ValidatedApplicationPublicationRequestV1 {
242 request,
243 unit,
244 entry_component: entry_component.clone(),
245 render_root_component: entry_component.clone(),
246 })
247}
248
249pub fn build_application_publication_product_v1(
258 validated: ValidatedApplicationPublicationRequestV1,
259) -> Result<ApplicationPublicationProductV1, ApplicationPublicationErrorV1> {
260 let asm =
261 ConstantFoldingPass.transform(&build_application_semantic_model_for_unit_with_packages(
262 &validated.unit,
263 &validated.request.package_contracts,
264 ));
265 build_application_publication_product_from_asm_v1(validated, asm)
266}
267
268pub fn build_application_publication_product_from_asm_v1(
277 validated: ValidatedApplicationPublicationRequestV1,
278 asm: crate::ApplicationSemanticModel,
279) -> Result<ApplicationPublicationProductV1, ApplicationPublicationErrorV1> {
280 let request = validated.request;
281 if let Some(diagnostic) = asm.diagnostics.iter().find(|diagnostic| {
282 diagnostic.code.starts_with("PSBIND10")
283 || matches!(diagnostic.code.as_str(), "PSC1128" | "PSC1130" | "PSC1131")
284 }) {
285 return Err(ApplicationPublicationErrorV1 {
286 code: "PSAPP2001_PACKAGE_SEMANTICS_INVALID",
287 message: format!("{}: {}", diagnostic.code, diagnostic.message),
288 });
289 }
290 let resource_runtime_artifact = if asm.resource_declarations.is_empty() {
291 None
292 } else {
293 let artifact =
294 build_runtime_resource_artifact_with_modules(&asm, &request.package_runtime_modules)
295 .map_err(|error| ApplicationPublicationErrorV1 {
296 code: "PSAPP2002_RESOURCE_RUNTIME_MAPPING_INCOMPLETE",
297 message: format!("{error:?}"),
298 })?;
299 if !validate_runtime_resource_artifact(&artifact).is_empty() {
300 return Err(ApplicationPublicationErrorV1 {
301 code: "PSAPP2003_RESOURCE_RUNTIME_ARTIFACT_INVALID",
302 message: "compiler produced an invalid Resource runtime artifact".into(),
303 });
304 }
305 if let Some(declaration) = artifact
306 .declarations
307 .iter()
308 .find(|declaration| declaration.execution_boundary == "Server")
309 {
310 return Err(ApplicationPublicationErrorV1 {
311 code: "PSAPP2004_SERVER_RESOURCE_IN_BROWSER_PUBLICATION",
312 message: format!(
313 "resource `{}` uses a server endpoint and cannot be published for browser execution",
314 declaration.id
315 ),
316 });
317 }
318 Some(artifact)
319 };
320 let opaque_runtime_artifact = if asm.opaque_action_resolutions.is_empty() {
321 None
322 } else {
323 let artifact =
324 build_runtime_opaque_artifact_with_modules(&asm, &request.package_runtime_modules)
325 .map_err(|error| ApplicationPublicationErrorV1 {
326 code: "PSAPP2005_OPAQUE_RUNTIME_MAPPING_INCOMPLETE",
327 message: format!("{error:?}"),
328 })?;
329 if !validate_runtime_opaque_artifact(&artifact).is_empty() {
330 return Err(ApplicationPublicationErrorV1 {
331 code: "PSAPP2006_OPAQUE_RUNTIME_ARTIFACT_INVALID",
332 message: "compiler produced an invalid opaque-terminal runtime artifact".into(),
333 });
334 }
335 Some(artifact)
336 };
337
338 let ir = lower_components_to_ir(&asm);
339 let computed_runtime_artifact = build_runtime_computed_artifact(&asm, &ir);
340 let computed_runtime_json = runtime_computed_artifact_json(&computed_runtime_artifact);
341 let effect_runtime_artifact =
342 build_runtime_effect_artifact(&asm, &optimize_effect_ir(&ir).output);
343 let effect_runtime_json = runtime_effect_artifact_json(&effect_runtime_artifact);
344 let context_runtime_artifact = build_runtime_context_artifact(&asm, &optimize_context_ir(&ir));
345 let context_runtime_json = runtime_context_artifact_json(&context_runtime_artifact);
346 let component_runtime_artifact =
347 build_runtime_component_artifact(&asm, &asm.component_ir_optimization);
348 let component_runtime_json = runtime_component_artifact_json(&component_runtime_artifact);
349 let forms_runtime_artifact = build_runtime_forms_artifact(&asm);
350 let forms_runtime_json = runtime_forms_artifact_json(&forms_runtime_artifact);
351 let resume_runtime_artifact = build_resume_manifest(&asm);
352 let resume_runtime_json = resume_manifest_json(&resume_runtime_artifact);
353 let resource_runtime_json = resource_runtime_artifact
354 .as_ref()
355 .map(runtime_resource_artifact_json);
356 let opaque_runtime_json = opaque_runtime_artifact
357 .as_ref()
358 .map(runtime_opaque_artifact_json);
359 let (production_chunk_graph, _) = extract_production_chunk_graph(
360 &SharedChunkCandidatePlan {
361 candidates: Vec::new(),
362 rejections: Vec::new(),
363 },
364 &production_root_chunk_inputs(&resume_runtime_artifact),
365 )
366 .map_err(|error| ApplicationPublicationErrorV1 {
367 code: "PSAPP2007_PRODUCTION_GRAPH_INVALID",
368 message: format!("{error:?}"),
369 })?;
370 let production_runtime_artifact =
371 build_production_runtime_artifact(&resume_runtime_artifact, &production_chunk_graph)
372 .map_err(|error| ApplicationPublicationErrorV1 {
373 code: "PSAPP2008_PRODUCTION_RUNTIME_INVALID",
374 message: format!("{error:?}"),
375 })?;
376 let production_runtime_json = production_runtime_artifact_json(&production_runtime_artifact);
377 let resume_chunks = build_resume_chunk_graph(&asm);
378 let html_fragment =
379 generate_ordinary_instance_html_for_component(&asm, &validated.render_root_component);
380 let template_manifest = build_template_manifest_from_asm(&asm);
381 let template_manifest_json = template_manifest_json(&template_manifest);
382 let page_title = asm
383 .components
384 .iter()
385 .find(|component| component.id == validated.render_root_component)
386 .map_or_else(
387 || "Presolve App".to_string(),
388 |component| component.class_name.clone(),
389 );
390 let page_html = generate_standalone_page_with_resume_runtime(
391 &page_title,
392 &html_fragment,
393 &template_manifest,
394 &computed_runtime_artifact,
395 &context_runtime_artifact,
396 &effect_runtime_artifact,
397 &component_runtime_artifact,
398 &forms_runtime_artifact,
399 &resume_runtime_artifact,
400 );
401 let page_html = resource_runtime_artifact
402 .as_ref()
403 .map_or(page_html, |resources| {
404 generate_standalone_page_with_resume_runtime_and_resources(
405 &page_title,
406 &html_fragment,
407 &template_manifest,
408 &computed_runtime_artifact,
409 &context_runtime_artifact,
410 &effect_runtime_artifact,
411 &component_runtime_artifact,
412 &forms_runtime_artifact,
413 &resume_runtime_artifact,
414 resources,
415 )
416 });
417 let page_html = if let Some(opaque) = &opaque_runtime_artifact {
418 embed_opaque_runtime_artifact(page_html, opaque)
419 } else {
420 page_html
421 };
422 let page_html = production_mode_page_html(
423 page_html,
424 request.profile == ApplicationPublicationProfileV1::Production,
425 &production_runtime_json,
426 );
427 let runtime_js = generate_runtime_stub();
428 let production_layout = emit_production_modules(&production_chunk_graph);
429 let development_bytes = byte_count([
430 &page_html,
431 &template_manifest_json,
432 &computed_runtime_json,
433 &context_runtime_json,
434 &effect_runtime_json,
435 &component_runtime_json,
436 &forms_runtime_json,
437 &resume_runtime_json,
438 &runtime_js,
439 ]) + resource_runtime_json
440 .as_ref()
441 .map_or(0, |json| json.len() as u64)
442 + opaque_runtime_json
443 .as_ref()
444 .map_or(0, |json| json.len() as u64);
445 let production_bytes = byte_count(
446 std::iter::once(&production_runtime_json).chain(
447 std::iter::once(&production_layout.eager)
448 .chain(production_layout.shared.iter())
449 .chain(production_layout.roots.iter())
450 .map(|module| &module.source),
451 ),
452 );
453 let report_inputs = ProductionReportInputs {
454 dead_products_removed: 0,
455 constants_pooled: 0,
456 programs_deduplicated: 0,
457 shared_candidates_rejected: 0,
458 binding_writes_coalesced: 0,
459 development_bytes,
460 production_bytes,
461 cold_init_operation_count: report_count(
462 resume_runtime_artifact
463 .capture_programs
464 .iter()
465 .map(|program| program.instructions.len())
466 .sum(),
467 ),
468 resume_restore_operation_count: report_count(
469 resume_runtime_artifact
470 .restore_programs
471 .iter()
472 .map(|program| program.instructions.len())
473 .sum(),
474 ),
475 max_action_batch_operation_count: 0,
476 max_scheduler_batch_width: 0,
477 max_dom_patch_count_per_action: 0,
478 retained_slot_count: report_count(resume_runtime_artifact.slot_schemas.len()),
479 };
480 let (optimization_report, runtime_cost_report) = build_production_reports(
481 &production_runtime_artifact,
482 &production_chunk_graph,
483 &report_inputs,
484 );
485
486 let mut artifacts = BTreeMap::new();
487 insert_artifact(&mut artifacts, "index.html", page_html.into_bytes());
488 insert_artifact(
489 &mut artifacts,
490 "template.manifest.json",
491 template_manifest_json.into_bytes(),
492 );
493 insert_artifact(
494 &mut artifacts,
495 "computed.runtime.json",
496 computed_runtime_json.into_bytes(),
497 );
498 insert_artifact(
499 &mut artifacts,
500 "context.runtime.json",
501 context_runtime_json.into_bytes(),
502 );
503 insert_artifact(
504 &mut artifacts,
505 "effect.runtime.json",
506 effect_runtime_json.into_bytes(),
507 );
508 insert_artifact(
509 &mut artifacts,
510 "component.runtime.json",
511 component_runtime_json.into_bytes(),
512 );
513 insert_artifact(
514 &mut artifacts,
515 "forms.runtime.json",
516 forms_runtime_json.into_bytes(),
517 );
518 insert_artifact(
519 &mut artifacts,
520 "resume.runtime.json",
521 resume_runtime_json.into_bytes(),
522 );
523 insert_artifact(
524 &mut artifacts,
525 "production.runtime.json",
526 production_runtime_json.into_bytes(),
527 );
528 insert_artifact(
529 &mut artifacts,
530 "optimization-report.json",
531 optimization_report_json(&optimization_report).into_bytes(),
532 );
533 insert_artifact(
534 &mut artifacts,
535 "runtime-cost-report.json",
536 runtime_cost_report_json(&runtime_cost_report).into_bytes(),
537 );
538 insert_artifact(&mut artifacts, "runtime.js", runtime_js.into_bytes());
539 if let Some(json) = resource_runtime_json {
540 insert_artifact(&mut artifacts, "resources.runtime.json", json.into_bytes());
541 }
542 if let Some(json) = opaque_runtime_json {
543 insert_artifact(&mut artifacts, "opaque.runtime.json", json.into_bytes());
544 }
545 for chunk in &resume_chunks.chunks {
546 artifacts.insert(
547 PathBuf::from(&chunk.module.module_path),
548 chunk.module.canonical_module_bytes.as_bytes().to_vec(),
549 );
550 }
551 if request.profile == ApplicationPublicationProfileV1::Production {
552 for module in std::iter::once(&production_layout.eager)
553 .chain(production_layout.shared.iter())
554 .chain(production_layout.roots.iter())
555 {
556 artifacts.insert(
557 PathBuf::from("production").join(&module.filename),
558 module.source.as_bytes().to_vec(),
559 );
560 }
561 }
562 let manifest = ApplicationPublicationManifestV1 {
563 schema_version: APPLICATION_PUBLICATION_MANIFEST_SCHEMA_VERSION,
564 compiler_contract: APPLICATION_PUBLICATION_COMPILER_CONTRACT_V1.into(),
565 workspace_snapshot_id: application_workspace_snapshot_id_v1(&request),
566 entry_component_id: validated.entry_component.to_string(),
567 profile: request.profile.as_str().into(),
568 artifacts: artifacts
569 .iter()
570 .map(|(path, bytes)| ApplicationPublicationArtifactV1 {
571 path: path.to_string_lossy().replace('\\', "/"),
572 digest: Digest::sha256(bytes).to_string(),
573 })
574 .collect(),
575 };
576 let manifest_json = application_publication_manifest_json_v1(&manifest);
577 insert_artifact(
578 &mut artifacts,
579 "application.manifest.json",
580 manifest_json.into_bytes(),
581 );
582 Ok(ApplicationPublicationProductV1 {
583 manifest,
584 artifacts,
585 })
586}
587
588fn insert_artifact(artifacts: &mut BTreeMap<PathBuf, Vec<u8>>, path: &str, bytes: Vec<u8>) {
589 let previous = artifacts.insert(PathBuf::from(path), bytes);
590 debug_assert!(
591 previous.is_none(),
592 "application artifact paths must be unique"
593 );
594}
595
596fn application_workspace_snapshot_id_v1(request: &ApplicationPublicationRequestV1) -> String {
597 let configuration = workspace_configuration_fingerprint_v1(&request.configuration)
598 .expect("validated application publication requires a valid workspace configuration");
599 let mut canonical = format!(
600 "contract={APPLICATION_PUBLICATION_COMPILER_CONTRACT_V1}\nconfiguration={}\n",
601 configuration.as_str()
602 );
603 let mut sources = request.sources.iter().collect::<Vec<_>>();
604 sources.sort_by(|left, right| left.logical_path.cmp(&right.logical_path));
605 for source in sources {
606 write!(
607 canonical,
608 "path={}\nsource={}\n",
609 source.logical_path.display(),
610 source.source
611 )
612 .expect("writing a workspace snapshot into a String cannot fail");
613 }
614 format!("application-workspace:{}", Digest::sha256(canonical))
615}
616
617fn production_root_chunk_inputs(
618 resume: &crate::ResumeManifest,
619) -> Vec<crate::ProductionRootChunkInput> {
620 let mut roots = resume
621 .chunks
622 .iter()
623 .filter_map(|chunk| {
624 let root_kind = match chunk.root_kind {
625 crate::resume_manifest::ResumeManifestChunkRootKind::Eager => return None,
626 crate::resume_manifest::ResumeManifestChunkRootKind::Interaction => "interaction",
627 crate::resume_manifest::ResumeManifestChunkRootKind::Visible => "visible",
628 crate::resume_manifest::ResumeManifestChunkRootKind::Manual => "manual",
629 };
630 Some(ProductionRootChunkInput {
631 activation_root_id: chunk.root_id.clone(),
632 root_kind: root_kind.into(),
633 programs: chunk
634 .provided_program_ids
635 .iter()
636 .map(|program| {
637 ExecutableProgramFingerprint::for_canonical_opcode_stream(
638 program.as_bytes(),
639 )
640 })
641 .collect(),
642 })
643 })
644 .collect::<Vec<_>>();
645 roots.sort_by(|left, right| left.activation_root_id.cmp(&right.activation_root_id));
646 roots
647}
648
649fn production_mode_page_html(
650 page_html: String,
651 production_mode: bool,
652 artifact_json: &str,
653) -> String {
654 if !production_mode {
655 return page_html;
656 }
657 let artifact = artifact_json.replace("</script", "<\\/script");
658 page_html.replacen(
659 " <script src=\"./runtime.js\" defer></script>",
660 &format!(
661 " <script type=\"application/json\" id=\"presolve-production-runtime\">{artifact} </script>\n <script src=\"./runtime.js\" defer></script>"
662 ),
663 1,
664 )
665}
666
667fn byte_count<'a>(values: impl IntoIterator<Item = &'a String>) -> u64 {
668 u64::try_from(values.into_iter().map(String::len).sum::<usize>())
669 .expect("application publication byte count exceeds u64")
670}
671
672fn report_count(value: usize) -> u32 {
673 u32::try_from(value).expect("application publication report count exceeds u32")
674}
675
676#[cfg(test)]
677mod tests {
678 use super::*;
679
680 fn request(sources: Vec<(&str, &str)>, entry: &str) -> ApplicationPublicationRequestV1 {
681 ApplicationPublicationRequestV1 {
682 configuration: WorkspaceConfiguration::default(),
683 sources: sources
684 .into_iter()
685 .map(|(logical_path, source)| ApplicationPublicationSourceV1 {
686 logical_path: PathBuf::from(logical_path),
687 source: source.into(),
688 })
689 .collect(),
690 entry_path: PathBuf::from(entry),
691 package_contracts: SemanticPackageResolutionTable::default(),
692 package_runtime_modules: SemanticPackageRuntimeModuleTable::default(),
693 profile: ApplicationPublicationProfileV1::Development,
694 output_root: PathBuf::from("dist"),
695 }
696 }
697
698 #[test]
699 fn validates_one_explicit_rendered_component_entry_independent_of_source_order() {
700 let source =
701 r#"@component("x-app") class App extends Component { render() { return <main />; } }"#;
702 let first = validate_application_publication_request_v1(request(
703 vec![
704 ("src/Utility.ts", "export const value = 1;"),
705 ("src/App.tsx", source),
706 ],
707 "src/App.tsx",
708 ))
709 .unwrap();
710 let second = validate_application_publication_request_v1(request(
711 vec![
712 ("src/App.tsx", source),
713 ("src/Utility.ts", "export const value = 1;"),
714 ],
715 "src/App.tsx",
716 ))
717 .unwrap();
718 assert_eq!(first.entry_component, second.entry_component);
719 }
720
721 #[test]
722 fn rejects_missing_ambiguous_and_non_member_entries() {
723 let missing = validate_application_publication_request_v1(request(
724 vec![("src/Entry.tsx", "export const value = 1;")],
725 "src/Entry.tsx",
726 ))
727 .unwrap_err();
728 assert_eq!(missing.code, "PSAPP1005_ENTRY_APPLICATION_ROOT_MISSING");
729 let ambiguous = validate_application_publication_request_v1(request(
730 vec![("src/Entry.tsx", r#"@component("x-a") class A extends Component { render() { return <main />; } } @component("x-b") class B extends Component { render() { return <main />; } }"#)],
731 "src/Entry.tsx",
732 ))
733 .unwrap_err();
734 assert_eq!(ambiguous.code, "PSAPP1006_ENTRY_APPLICATION_ROOT_AMBIGUOUS");
735 let non_member = validate_application_publication_request_v1(request(
736 vec![("src/App.tsx", r#"@component("x-app") class App extends Component { render() { return <main />; } }"#)],
737 "src/Missing.tsx",
738 ))
739 .unwrap_err();
740 assert_eq!(non_member.code, "PSAPP1004_ENTRY_NOT_IN_SOURCE_SET");
741 }
742
743 #[test]
744 fn lowers_a_validated_complete_workspace_to_a_digest_bound_manifest() {
745 let request = request(
746 vec![
747 ("src/Helper.ts", "const value = 1;"),
748 (
749 "src/App.tsx",
750 r#"@component("x-app") class App extends Component { render() { return <main>App</main>; } }"#,
751 ),
752 ],
753 "src/App.tsx",
754 );
755 let product = build_application_publication_product_v1(
756 validate_application_publication_request_v1(request).unwrap(),
757 )
758 .unwrap();
759 assert_eq!(product.manifest.schema_version, 1);
760 assert_eq!(product.manifest.profile, "development");
761 assert!(product.artifacts.contains_key(&PathBuf::from("index.html")));
762 assert!(product
763 .artifacts
764 .contains_key(&PathBuf::from("application.manifest.json")));
765 assert_eq!(
766 product.manifest.artifacts.len() + 1,
767 product.artifacts.len(),
768 "the manifest inventories every generated artifact except itself"
769 );
770 for artifact in &product.manifest.artifacts {
771 let bytes = product
772 .artifacts
773 .get(&PathBuf::from(&artifact.path))
774 .unwrap();
775 assert_eq!(artifact.digest, Digest::sha256(bytes).to_string());
776 }
777 }
778
779 #[test]
780 fn materializes_only_the_selected_entry_tree_from_a_multi_component_workspace() {
781 let request = request(
782 vec![
783 (
784 "src/Home.tsx",
785 r#"@component("x-home") class Home extends Component { render() { return <main>Home</main>; } }"#,
786 ),
787 (
788 "src/About.tsx",
789 r#"@component("x-about") class About extends Component { render() { return <main>About</main>; } }"#,
790 ),
791 ],
792 "src/Home.tsx",
793 );
794
795 let product = build_application_publication_product_v1(
796 validate_application_publication_request_v1(request).unwrap(),
797 )
798 .unwrap();
799 let page =
800 String::from_utf8(product.artifacts[&PathBuf::from("index.html")].clone()).unwrap();
801
802 assert!(page.contains(">Home</main>"));
803 assert!(!page.contains(">About</main>"));
804 }
805}