Skip to main content

presolve_compiler/
application_publication.rs

1//! Phase P explicit multi-source application-publication request validation.
2//!
3//! This module deliberately stops before artifact lowering or filesystem
4//! publication. It establishes one compiler-owned request/entry authority for
5//! the later publication product.
6
7#![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    /// The complete caller-authorized source set. The compiler parses this
71    /// exact set; no source-root discovery is part of publication.
72    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    /// Source-selected page component. This remains the published entry
85    /// identity even when a compiler-owned file-route layout is the rendered
86    /// root.
87    pub entry_component: SemanticId,
88    /// Compiler-selected materialization root. Explicit publication defaults
89    /// to `entry_component`; conventional file routes may set an outer layout.
90    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    /// Exact compiler-generated bytes keyed by normalized relative artifact
141    /// path. Filesystem publication is deliberately a separate host concern.
142    pub artifacts: BTreeMap<PathBuf, Vec<u8>>,
143}
144
145#[must_use]
146/// Serializes the exact schema-v1 publication manifest.
147///
148/// # Panics
149///
150/// Panics only if the compiler-owned manifest model cannot serialize.
151pub 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
159/// Validates caller-owned request identity and explicit entry selection.
160///
161/// # Errors
162///
163/// Returns a stable `PSAPP100x` error when the workspace configuration, exact
164/// source set, logical entry, or rendered application root is invalid.
165pub 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
249/// Lowers one validated, explicit complete workspace into its exact artifact
250/// inventory. This is the only multi-source publication derivation authority;
251/// command and framework layers may publish these bytes but may not alter them.
252///
253/// # Errors
254///
255/// Returns a stable `PSAPP200x` error when package mappings or compiler
256/// generated runtime/production products cannot form a browser publication.
257pub 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
268/// Lowers one validated explicit publication request from an already-built
269/// canonical application model. This is used by compiler-owned file-route
270/// composition; callers cannot supply altered artifacts or a framework model.
271///
272/// # Errors
273///
274/// Returns the same publication diagnostics as the standard model assembly
275/// path when compiler products cannot form a browser publication.
276pub 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}