presolve_compiler/
runtime_package_invocation_artifact.rs1use std::collections::BTreeSet;
4
5use serde::{Deserialize, Serialize};
6
7use crate::ApplicationSemanticModel;
8
9pub const RUNTIME_PACKAGE_INVOCATION_ARTIFACT_SCHEMA_VERSION: u32 = 1;
10pub const PACKAGE_INVOCATION_REGISTRY_PATH: &str = "/presolve.package-invocations.js";
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(deny_unknown_fields)]
14pub struct RuntimePackageInvocationArtifact {
15 pub schema_version: u32,
16 pub registry_path: String,
17 pub invocations: Vec<RuntimePackageInvocation>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct RuntimePackageInvocation {
23 pub id: String,
24 pub owner_component: String,
25 pub method: String,
26 pub module_specifier: String,
27 pub export: String,
28 pub owner_source: String,
29 pub declaration_modules: Vec<String>,
30 pub execution_boundary: String,
31 pub resume_policy: String,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum RuntimePackageInvocationArtifactValidationError {
36 UnsupportedSchemaVersion { actual: u32 },
37 InvalidRegistryPath,
38 DuplicateInvocation { id: String },
39 MissingCoordinate { id: String },
40 InvalidLifecycle { id: String },
41}
42
43#[must_use]
44pub fn build_runtime_package_invocation_artifact(
45 model: &ApplicationSemanticModel,
46) -> RuntimePackageInvocationArtifact {
47 let mut invocations = model
48 .terminal_package_invocations
49 .iter()
50 .map(|fact| {
51 let mut declaration_modules = fact.declaration_modules.clone();
52 declaration_modules.sort();
53 declaration_modules.dedup();
54 RuntimePackageInvocation {
55 id: fact.id.to_string(),
56 owner_component: fact.owner_component.to_string(),
57 method: fact.method.to_string(),
58 module_specifier: fact.module_specifier.clone(),
59 export: fact.export_name.clone(),
60 owner_source: fact.provenance.path.to_string_lossy().into_owned(),
61 declaration_modules,
62 execution_boundary: "client".into(),
63 resume_policy: "event_restore".into(),
64 }
65 })
66 .collect::<Vec<_>>();
67 invocations.sort_by(|left, right| left.id.cmp(&right.id));
68 RuntimePackageInvocationArtifact {
69 schema_version: RUNTIME_PACKAGE_INVOCATION_ARTIFACT_SCHEMA_VERSION,
70 registry_path: PACKAGE_INVOCATION_REGISTRY_PATH.into(),
71 invocations,
72 }
73}
74
75#[must_use]
76pub fn runtime_package_invocation_artifact_json(
77 artifact: &RuntimePackageInvocationArtifact,
78) -> String {
79 serde_json::to_string_pretty(artifact).expect("package invocation artifact should serialize")
80 + "\n"
81}
82
83#[must_use]
84pub fn validate_runtime_package_invocation_artifact(
85 artifact: &RuntimePackageInvocationArtifact,
86) -> Vec<RuntimePackageInvocationArtifactValidationError> {
87 let mut errors = Vec::new();
88 if artifact.schema_version != RUNTIME_PACKAGE_INVOCATION_ARTIFACT_SCHEMA_VERSION {
89 errors.push(
90 RuntimePackageInvocationArtifactValidationError::UnsupportedSchemaVersion {
91 actual: artifact.schema_version,
92 },
93 );
94 }
95 if artifact.registry_path != PACKAGE_INVOCATION_REGISTRY_PATH {
96 errors.push(RuntimePackageInvocationArtifactValidationError::InvalidRegistryPath);
97 }
98 let mut ids = BTreeSet::new();
99 for invocation in &artifact.invocations {
100 if !ids.insert(invocation.id.clone()) {
101 errors.push(
102 RuntimePackageInvocationArtifactValidationError::DuplicateInvocation {
103 id: invocation.id.clone(),
104 },
105 );
106 }
107 if invocation.id.is_empty()
108 || invocation.owner_component.is_empty()
109 || invocation.method.is_empty()
110 || invocation.module_specifier.is_empty()
111 || invocation.export.is_empty()
112 || invocation.owner_source.is_empty()
113 || invocation.declaration_modules.is_empty()
114 {
115 errors.push(
116 RuntimePackageInvocationArtifactValidationError::MissingCoordinate {
117 id: invocation.id.clone(),
118 },
119 );
120 }
121 if invocation.execution_boundary != "client" || invocation.resume_policy != "event_restore"
122 {
123 errors.push(
124 RuntimePackageInvocationArtifactValidationError::InvalidLifecycle {
125 id: invocation.id.clone(),
126 },
127 );
128 }
129 }
130 errors
131}
132
133#[cfg(test)]
134mod tests {
135 use super::{
136 runtime_package_invocation_artifact_json, validate_runtime_package_invocation_artifact,
137 RuntimePackageInvocation, RuntimePackageInvocationArtifact,
138 RuntimePackageInvocationArtifactValidationError, PACKAGE_INVOCATION_REGISTRY_PATH,
139 RUNTIME_PACKAGE_INVOCATION_ARTIFACT_SCHEMA_VERSION,
140 };
141
142 fn artifact() -> RuntimePackageInvocationArtifact {
143 RuntimePackageInvocationArtifact {
144 schema_version: RUNTIME_PACKAGE_INVOCATION_ARTIFACT_SCHEMA_VERSION,
145 registry_path: PACKAGE_INVOCATION_REGISTRY_PATH.into(),
146 invocations: vec![RuntimePackageInvocation {
147 id: "component:Home/package-invocation:record".into(),
148 owner_component: "component:Home".into(),
149 method: "component:Home/action-endpoint:record".into(),
150 module_specifier: "analytics-kit".into(),
151 export: "recordVisit".into(),
152 owner_source: "app/routes/index.tsx".into(),
153 declaration_modules: vec!["node_modules/analytics-kit/index.d.ts".into()],
154 execution_boundary: "client".into(),
155 resume_policy: "event_restore".into(),
156 }],
157 }
158 }
159
160 #[test]
161 fn validates_and_serializes_exact_package_invocation_coordinates() {
162 let artifact = artifact();
163 assert!(validate_runtime_package_invocation_artifact(&artifact).is_empty());
164 let json = runtime_package_invocation_artifact_json(&artifact);
165 assert!(json.contains("\"registry_path\": \"/presolve.package-invocations.js\""));
166 assert!(json.contains("\"module_specifier\": \"analytics-kit\""));
167 }
168
169 #[test]
170 fn rejects_duplicate_missing_and_noncanonical_package_invocations() {
171 let mut artifact = artifact();
172 artifact.schema_version += 1;
173 artifact.registry_path = "relative.js".into();
174 artifact.invocations[0].module_specifier.clear();
175 artifact.invocations[0].resume_policy = "snapshot".into();
176 artifact.invocations.push(artifact.invocations[0].clone());
177 let errors = validate_runtime_package_invocation_artifact(&artifact);
178 assert!(errors.iter().any(|error| matches!(
179 error,
180 RuntimePackageInvocationArtifactValidationError::UnsupportedSchemaVersion { .. }
181 )));
182 assert!(errors.iter().any(|error| matches!(
183 error,
184 RuntimePackageInvocationArtifactValidationError::InvalidRegistryPath
185 )));
186 assert!(errors.iter().any(|error| matches!(
187 error,
188 RuntimePackageInvocationArtifactValidationError::DuplicateInvocation { .. }
189 )));
190 assert!(errors.iter().any(|error| matches!(
191 error,
192 RuntimePackageInvocationArtifactValidationError::MissingCoordinate { .. }
193 )));
194 assert!(errors.iter().any(|error| matches!(
195 error,
196 RuntimePackageInvocationArtifactValidationError::InvalidLifecycle { .. }
197 )));
198 }
199}