Skip to main content

presolve_compiler/
file_route_publication.rs

1//! Compiler-owned publication for a discovered ergonomic file-route project.
2
3use std::collections::BTreeMap;
4use std::fmt;
5use std::path::PathBuf;
6
7use serde::Serialize;
8
9use crate::{
10    build_application_publication_product_from_asm_v1,
11    build_application_semantic_model_for_unit_with_packages, build_binding_table_with_packages,
12    build_file_route_application_semantic_model_for_route_with_packages,
13    build_file_route_application_semantic_model_for_route_with_packages_and_v2_authoring,
14    build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring,
15    build_layout_composition_plan_v1, build_module_graph, build_route_loader_plan_v1,
16    build_route_server_action_plan_v1, build_symbol_table, build_validated_file_route_graph_v1,
17    route_loader_plan_json_v1, route_server_action_plan_json_v1,
18    validate_application_publication_request_v1, ApplicationPublicationArtifactV1,
19    ApplicationPublicationErrorV1, ApplicationPublicationProfileV1,
20    ApplicationPublicationRequestErrorV1, ApplicationPublicationRequestV1,
21    ApplicationPublicationSourceV1, CompilationUnit, ConstantFoldingPass, ImmutableAsmPass,
22    SemanticPackageResolutionTable, SemanticPackageRuntimeModuleTable,
23    ValidatedApplicationPublicationRequestV1,
24};
25
26pub const FILE_ROUTE_PUBLICATION_MANIFEST_SCHEMA_VERSION: u32 = 1;
27pub const FILE_ROUTE_PUBLICATION_COMPILER_CONTRACT_V1: &str = "presolve-file-route-publication:1";
28pub const FILE_ROUTE_ENVIRONMENT_ARTIFACT_PATH_V1: &str = "environment.browser.json";
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct FileRoutePublicationRequestV1 {
32    pub configuration: crate::platform::WorkspaceConfiguration,
33    pub sources: Vec<ApplicationPublicationSourceV1>,
34    pub package_contracts: SemanticPackageResolutionTable,
35    pub package_runtime_modules: SemanticPackageRuntimeModuleTable,
36    pub profile: ApplicationPublicationProfileV1,
37    pub output_root: PathBuf,
38    /// Optional caller-selected compiler environment artifact. Its contents
39    /// are validated here but must have been built from environment-read
40    /// lowering rather than from ambient configuration.
41    pub environment_artifact: Option<crate::EnvironmentPublicationArtifactV1>,
42    /// Compiler-validated V2 authoring evidence keyed by source path.
43    pub v2_authoring: BTreeMap<PathBuf, crate::CanonicalAuthoredSemanticModelV1>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct FileRoutePublicationErrorV1 {
48    pub code: &'static str,
49    pub message: String,
50}
51
52impl fmt::Display for FileRoutePublicationErrorV1 {
53    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
54        write!(output, "{}: {}", self.code, self.message)
55    }
56}
57
58impl std::error::Error for FileRoutePublicationErrorV1 {}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
61pub struct FileRoutePublicationManifestV1 {
62    pub schema_version: u32,
63    pub compiler_contract: String,
64    pub profile: String,
65    pub routes: Vec<FileRoutePublicationRouteV1>,
66    pub artifacts: Vec<ApplicationPublicationArtifactV1>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
70pub struct FileRoutePublicationRouteV1 {
71    pub path: String,
72    pub entry_component_id: String,
73    pub artifact_root: String,
74    pub layout_component_ids: Vec<String>,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct FileRoutePublicationProductV1 {
79    pub manifest: FileRoutePublicationManifestV1,
80    pub artifacts: BTreeMap<PathBuf, Vec<u8>>,
81    /// Internal compiler-selected route sources for sidecar joins. This is not
82    /// serialized and must not be reconstructed by adapters.
83    pub route_source_paths: BTreeMap<String, PathBuf>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum FileRouteRequestTargetV1 {
88    Redirect { location: String },
89    Artifact { path: PathBuf },
90}
91
92/// Compiler-resolved request facts for one conventional file route. Server
93/// adapters consume this record instead of re-matching route patterns or
94/// deriving parameter names from source paths.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct FileRouteRequestMatchV1 {
97    pub route_path: String,
98    pub entry_component_id: String,
99    /// Exact non-empty path-segment values keyed by the compiler-issued route
100    /// parameter name. Values remain percent-encoded request segments; URL
101    /// decoding belongs to a later explicit normalization contract.
102    pub parameters: BTreeMap<String, String>,
103    pub target: FileRouteRequestTargetV1,
104}
105
106/// Builds exact compiler artifacts for every validated `app/routes` page.
107///
108/// Every page is lowered through the existing explicit application-publication
109/// product using its compiler-selected source module as the entry. The product
110/// does not interpret route source or merge generated bytes.
111pub fn build_file_route_publication_v1(
112    request: FileRoutePublicationRequestV1,
113) -> Result<FileRoutePublicationProductV1, FileRoutePublicationErrorV1> {
114    if let Some(artifact) = &request.environment_artifact {
115        crate::validate_environment_publication_artifact_v1(artifact).map_err(|error| {
116            FileRoutePublicationErrorV1 {
117                code: "PSENV1201_ARTIFACT_INVALID",
118                message: error.to_string(),
119            }
120        })?;
121    }
122    if request.sources.is_empty() {
123        return Err(FileRoutePublicationErrorV1 {
124            code: "PSROUTE2001_EMPTY_FILE_ROUTE_SOURCE_SET",
125            message: "file-route publication requires discovered application sources".into(),
126        });
127    }
128    let unit = CompilationUnit::parse_sources(
129        request
130            .sources
131            .iter()
132            .map(|source| (&source.logical_path, source.source.as_str())),
133    );
134    let model = if request.v2_authoring.is_empty() {
135        build_application_semantic_model_for_unit_with_packages(&unit, &request.package_contracts)
136    } else {
137        build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring(
138            &unit,
139            &request.package_contracts,
140            &request.v2_authoring,
141        )
142        .map_err(|error| FileRoutePublicationErrorV1 {
143            code: error.code,
144            message: error.message,
145        })?
146    };
147    let graph = build_validated_file_route_graph_v1(&model).map_err(route_error)?;
148    build_layout_composition_plan_v1(&model, &graph).map_err(|error| {
149        FileRoutePublicationErrorV1 {
150            code: error.code,
151            message: error.message,
152        }
153    })?;
154    let symbols = build_symbol_table(&unit);
155    let modules = build_module_graph(&unit);
156    let bindings =
157        build_binding_table_with_packages(&unit, &symbols, &modules, &request.package_contracts);
158    let route_loader_plan = build_route_loader_plan_v1(&model.components, &graph, &bindings)
159        .map_err(|error| FileRoutePublicationErrorV1 {
160            code: error.code,
161            message: error.message,
162        })?;
163    let route_server_action_plan =
164        build_route_server_action_plan_v1(&model.components, &graph, &bindings).map_err(
165            |error| FileRoutePublicationErrorV1 {
166                code: error.code,
167                message: error.message,
168            },
169        )?;
170    if graph.routes.is_empty() {
171        return Err(FileRoutePublicationErrorV1 {
172            code: "PSROUTE2002_FILE_ROUTE_SET_EMPTY",
173            message: "no rendered components were discovered below app/routes".into(),
174        });
175    }
176
177    let component_modules = model
178        .components
179        .iter()
180        .map(|component| (component.id.clone(), component.module_path.clone()))
181        .collect::<BTreeMap<_, _>>();
182    let mut artifacts = BTreeMap::new();
183    let mut routes = Vec::new();
184    let mut route_source_paths = BTreeMap::new();
185    for route in &graph.routes {
186        let entry_path =
187            component_modules
188                .get(&route.component)
189                .ok_or_else(|| FileRoutePublicationErrorV1 {
190                    code: "PSROUTE2003_ROUTE_COMPONENT_MODULE_MISSING",
191                    message: route.component.to_string(),
192                })?;
193        let publication_request = ApplicationPublicationRequestV1 {
194            configuration: request.configuration.clone(),
195            sources: request.sources.clone(),
196            entry_path: entry_path.clone(),
197            package_contracts: request.package_contracts.clone(),
198            package_runtime_modules: request.package_runtime_modules.clone(),
199            profile: request.profile,
200            output_root: request.output_root.clone(),
201        };
202        let mut validated = if request.v2_authoring.is_empty() {
203            validate_application_publication_request_v1(publication_request)
204                .map_err(application_request_error)?
205        } else {
206            ValidatedApplicationPublicationRequestV1 {
207                request: publication_request,
208                unit: unit.clone(),
209                entry_component: route.component.clone(),
210                render_root_component: route.component.clone(),
211            }
212        };
213        let route_model = if request.v2_authoring.is_empty() {
214            build_file_route_application_semantic_model_for_route_with_packages(
215                &unit,
216                &request.package_contracts,
217                &route.component,
218            )
219        } else {
220            build_file_route_application_semantic_model_for_route_with_packages_and_v2_authoring(
221                &unit,
222                &request.package_contracts,
223                &route.component,
224                &request.v2_authoring,
225            )
226        }
227        .map_err(|error| FileRoutePublicationErrorV1 {
228            code: error.code,
229            message: error.message,
230        })?;
231        let composed = ConstantFoldingPass.transform(&route_model);
232        validated.render_root_component = route
233            .layouts
234            .first()
235            .cloned()
236            .unwrap_or_else(|| route.component.clone());
237        let product = build_application_publication_product_from_asm_v1(validated, composed)
238            .map_err(application_product_error)?;
239        let artifact_root = file_route_artifact_root_v1(&route.path);
240        for (path, bytes) in product.artifacts {
241            let path = PathBuf::from(&artifact_root).join(path);
242            if artifacts.insert(path.clone(), bytes).is_some() {
243                return Err(FileRoutePublicationErrorV1 {
244                    code: "PSROUTE2004_FILE_ROUTE_ARTIFACT_COLLISION",
245                    message: path.display().to_string(),
246                });
247            }
248        }
249        routes.push(FileRoutePublicationRouteV1 {
250            path: route.path.clone(),
251            entry_component_id: route.component.to_string(),
252            artifact_root,
253            layout_component_ids: route.layouts.iter().map(ToString::to_string).collect(),
254        });
255        route_source_paths.insert(route.path.clone(), entry_path.clone());
256    }
257    routes.sort_by(|left, right| left.path.cmp(&right.path));
258    artifacts.insert(
259        PathBuf::from("route-loaders.plan.json"),
260        route_loader_plan_json_v1(&route_loader_plan).into_bytes(),
261    );
262    artifacts.insert(
263        PathBuf::from("route-server-actions.plan.json"),
264        route_server_action_plan_json_v1(&route_server_action_plan).into_bytes(),
265    );
266    if let Some(artifact) = request.environment_artifact {
267        artifacts.insert(
268            PathBuf::from(FILE_ROUTE_ENVIRONMENT_ARTIFACT_PATH_V1),
269            crate::environment_publication_artifact_json_v1(&artifact).into_bytes(),
270        );
271    }
272    let manifest = FileRoutePublicationManifestV1 {
273        schema_version: FILE_ROUTE_PUBLICATION_MANIFEST_SCHEMA_VERSION,
274        compiler_contract: FILE_ROUTE_PUBLICATION_COMPILER_CONTRACT_V1.into(),
275        profile: request.profile.as_str().into(),
276        routes,
277        artifacts: artifacts
278            .iter()
279            .map(|(path, bytes)| ApplicationPublicationArtifactV1 {
280                path: path.to_string_lossy().replace('\\', "/"),
281                digest: crate::platform::Digest::sha256(bytes).to_string(),
282            })
283            .collect(),
284    };
285    artifacts.insert(
286        PathBuf::from("file-routes.manifest.json"),
287        file_route_publication_manifest_json_v1(&manifest).into_bytes(),
288    );
289    Ok(FileRoutePublicationProductV1 {
290        manifest,
291        artifacts,
292        route_source_paths,
293    })
294}
295
296#[must_use]
297pub fn file_route_publication_manifest_json_v1(value: &FileRoutePublicationManifestV1) -> String {
298    serde_json::to_string_pretty(value).expect("file-route publication manifest serializes") + "\n"
299}
300
301#[must_use]
302pub fn file_route_artifact_root_v1(path: &str) -> String {
303    if path == "/" {
304        return "routes/root".into();
305    }
306    let segments = path
307        .trim_matches('/')
308        .split('/')
309        .map(|segment| {
310            if let Some(parameter) = segment.strip_prefix(':') {
311                format!("parameter-{parameter}")
312            } else {
313                format!("segment-{segment}")
314            }
315        })
316        .collect::<Vec<_>>();
317    format!("routes/{}", segments.join("/"))
318}
319
320/// Resolves one HTTP path using only the compiler-issued route publication
321/// manifest. The returned artifact remains an opaque compiler byte path for
322/// the host to serve.
323#[must_use]
324pub fn resolve_file_route_request_v1(
325    manifest: &FileRoutePublicationManifestV1,
326    request_path: &str,
327) -> Option<FileRouteRequestTargetV1> {
328    resolve_file_route_request_match_v1(manifest, request_path).map(|match_| match_.target)
329}
330
331/// Resolves a request through compiler-issued route topology and retains the
332/// selected route identity plus exact dynamic parameter values. It does not
333/// execute application code or interpret query, header, or body data.
334#[must_use]
335pub fn resolve_file_route_request_match_v1(
336    manifest: &FileRoutePublicationManifestV1,
337    request_path: &str,
338) -> Option<FileRouteRequestMatchV1> {
339    if !request_path.starts_with('/') || request_path.contains('?') || request_path.contains('#') {
340        return None;
341    }
342    let trailing_slash = request_path.ends_with('/');
343    let segments = request_path
344        .trim_matches('/')
345        .split('/')
346        .filter(|segment| !segment.is_empty())
347        .collect::<Vec<_>>();
348    let route = manifest
349        .routes
350        .iter()
351        .filter_map(|route| {
352            let route_segments = route
353                .path
354                .trim_matches('/')
355                .split('/')
356                .filter(|segment| !segment.is_empty())
357                .collect::<Vec<_>>();
358            (segments.len() >= route_segments.len()
359                && route_segments
360                    .iter()
361                    .zip(&segments)
362                    .all(|(route, request)| {
363                        route.strip_prefix(':').is_some_and(|_| !request.is_empty())
364                            || route == request
365                    }))
366            .then_some((route, route_segments))
367        })
368        .max_by(|(_, left_segments), (_, right_segments)| {
369            route_match_score(left_segments).cmp(&route_match_score(right_segments))
370        })?;
371    let (route, route_segments) = route;
372    let parameters = route_segments
373        .iter()
374        .zip(&segments)
375        .filter_map(|(route_segment, request_segment)| {
376            route_segment
377                .strip_prefix(':')
378                .map(|name| (name.to_string(), (*request_segment).to_string()))
379        })
380        .collect::<BTreeMap<_, _>>();
381    let match_record = |target| FileRouteRequestMatchV1 {
382        route_path: route.path.clone(),
383        entry_component_id: route.entry_component_id.clone(),
384        parameters: parameters.clone(),
385        target,
386    };
387    if segments.len() == route_segments.len() {
388        if route.path != "/" && !trailing_slash {
389            return Some(match_record(FileRouteRequestTargetV1::Redirect {
390                location: format!("{request_path}/"),
391            }));
392        }
393        return Some(match_record(FileRouteRequestTargetV1::Artifact {
394            path: PathBuf::from(&route.artifact_root).join("index.html"),
395        }));
396    }
397    let suffix = &segments[route_segments.len()..];
398    suffix
399        .iter()
400        .all(|segment| is_safe_request_asset_segment(segment))
401        .then(|| {
402            match_record(FileRouteRequestTargetV1::Artifact {
403                path: PathBuf::from(&route.artifact_root).join(suffix.iter().collect::<PathBuf>()),
404            })
405        })
406}
407
408fn route_match_score(segments: &[&str]) -> (usize, usize) {
409    (
410        segments
411            .iter()
412            .filter(|segment| !segment.starts_with(':'))
413            .count(),
414        segments.len(),
415    )
416}
417
418fn is_safe_request_asset_segment(segment: &str) -> bool {
419    !segment.is_empty() && segment != "." && segment != ".." && !segment.contains('\\')
420}
421
422fn route_error(error: crate::RouteGraphError) -> FileRoutePublicationErrorV1 {
423    FileRoutePublicationErrorV1 {
424        code: error.code,
425        message: error.message,
426    }
427}
428
429fn application_request_error(
430    error: ApplicationPublicationRequestErrorV1,
431) -> FileRoutePublicationErrorV1 {
432    FileRoutePublicationErrorV1 {
433        code: error.code,
434        message: error.message,
435    }
436}
437
438fn application_product_error(error: ApplicationPublicationErrorV1) -> FileRoutePublicationErrorV1 {
439    FileRoutePublicationErrorV1 {
440        code: error.code,
441        message: error.message,
442    }
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448
449    #[test]
450    fn publishes_each_file_route_under_a_distinct_compiler_owned_root() {
451        let product = build_file_route_publication_v1(FileRoutePublicationRequestV1 {
452            configuration: crate::platform::WorkspaceConfiguration::default(),
453            sources: vec![
454                ApplicationPublicationSourceV1 {
455                    logical_path: "app/routes/index.tsx".into(),
456                    source: r#"@component() class Home extends Component { render() { return <main>Home</main>; } }"#.into(),
457                },
458                ApplicationPublicationSourceV1 {
459                    logical_path: "app/routes/about.tsx".into(),
460                    source: r#"@component() class About extends Component { render() { return <main>About</main>; } }"#.into(),
461                },
462            ],
463            package_contracts: SemanticPackageResolutionTable::default(),
464            package_runtime_modules: SemanticPackageRuntimeModuleTable::default(),
465            profile: ApplicationPublicationProfileV1::Development,
466            output_root: "dist".into(),
467            environment_artifact: Some(crate::EnvironmentPublicationArtifactV1 {
468                schema_version: crate::ENVIRONMENT_PUBLICATION_SCHEMA_VERSION,
469                browser_values: BTreeMap::from([(
470                    "PRESOLVE_PUBLIC_NAME".into(),
471                    "Presolve".into(),
472                )]),
473            }),
474            v2_authoring: BTreeMap::new(),
475        })
476        .unwrap();
477
478        assert_eq!(product.manifest.routes.len(), 2);
479        let home =
480            String::from_utf8(product.artifacts[&PathBuf::from("routes/root/index.html")].clone())
481                .unwrap();
482        let about = String::from_utf8(
483            product.artifacts[&PathBuf::from("routes/segment-about/index.html")].clone(),
484        )
485        .unwrap();
486        assert!(home.contains(">Home</main>"));
487        assert!(!home.contains(">About</main>"));
488        assert!(about.contains(">About</main>"));
489        assert!(product
490            .artifacts
491            .contains_key(&PathBuf::from("file-routes.manifest.json")));
492        let environment = String::from_utf8(
493            product.artifacts[&PathBuf::from(FILE_ROUTE_ENVIRONMENT_ARTIFACT_PATH_V1)].clone(),
494        )
495        .unwrap();
496        assert!(environment.contains("PRESOLVE_PUBLIC_NAME"));
497        assert!(product
498            .manifest
499            .artifacts
500            .iter()
501            .any(|artifact| artifact.path == FILE_ROUTE_ENVIRONMENT_ARTIFACT_PATH_V1));
502    }
503
504    #[test]
505    fn publishes_a_route_through_its_compiler_composed_layout_root() {
506        let product = build_file_route_publication_v1(FileRoutePublicationRequestV1 {
507            configuration: crate::platform::WorkspaceConfiguration::default(),
508            sources: vec![
509                ApplicationPublicationSourceV1 {
510                    logical_path: "app/layout.tsx".into(),
511                    source: r#"
512@component() class AppLayout extends Component {
513  @slot() children!: SlotContent;
514  render() { return <main><slot /></main>; }
515}
516"#
517                    .into(),
518                },
519                ApplicationPublicationSourceV1 {
520                    logical_path: "app/routes/index.tsx".into(),
521                    source: r#"@component() class Home extends Component { render() { return <article>Home</article>; } }"#.into(),
522                },
523            ],
524            package_contracts: SemanticPackageResolutionTable::default(),
525            package_runtime_modules: SemanticPackageRuntimeModuleTable::default(),
526            profile: ApplicationPublicationProfileV1::Development,
527            output_root: "dist".into(),
528            environment_artifact: None,
529            v2_authoring: BTreeMap::new(),
530        })
531        .expect("valid route layout publication");
532
533        let html =
534            String::from_utf8(product.artifacts[&PathBuf::from("routes/root/index.html")].clone())
535                .unwrap();
536        assert!(html.contains("<main"));
537        assert!(html.contains("<article"));
538        assert!(html.contains("Home"));
539    }
540
541    #[test]
542    fn publishes_an_exact_route_loader_handoff_plan_without_server_execution() {
543        let contract = crate::parse_semantic_package_contract(
544            r#"{"schema_version":1,"package":"post-service","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"loadPost":{"kind":"resource","type_signature":"RouteParameters -> Resource<Post, NotFound>","runtime_module":"dist/load-post.js","resume_policy":"reload","resource_endpoint":{"execution_boundary":"server","cancellation":"abort","resume":"reload"},"route_loader":{"input":"route_parameters","cache":{"scope":"public","max_age_seconds":60},"failure":"typed"}}}}"#,
545        )
546        .unwrap();
547        let mut contracts = SemanticPackageResolutionTable::default();
548        contracts.insert("post-service".into(), contract).unwrap();
549        let product = build_file_route_publication_v1(FileRoutePublicationRequestV1 {
550            configuration: crate::platform::WorkspaceConfiguration::default(),
551            sources: vec![ApplicationPublicationSourceV1 {
552                logical_path: "app/routes/posts/[slug].tsx".into(),
553                source: r#"
554import { loadPost } from "post-service";
555@component() class Post {
556  @loader("loadPost") post!: Resource<Post, NotFound>;
557  render() { return <article />; }
558}
559"#
560                .into(),
561            }],
562            package_contracts: contracts,
563            package_runtime_modules: SemanticPackageRuntimeModuleTable::default(),
564            profile: ApplicationPublicationProfileV1::Development,
565            output_root: "dist".into(),
566            environment_artifact: None,
567            v2_authoring: BTreeMap::new(),
568        })
569        .expect("route loader handoff publication");
570
571        let plan =
572            String::from_utf8(product.artifacts[&PathBuf::from("route-loaders.plan.json")].clone())
573                .unwrap();
574        assert!(plan.contains("post-service"));
575        assert!(plan.contains("route_parameters"));
576        assert!(plan.contains("max_age_seconds"));
577        assert!(!plan.contains("function loadPost"));
578    }
579
580    #[test]
581    fn publishes_an_exact_route_server_action_handoff_without_server_execution() {
582        let contract = crate::parse_semantic_package_contract(
583            r#"{"schema_version":1,"package":"post-service","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"savePost":{"kind":"server_action","type_signature":"FormData -> ServerActionResult","runtime_module":"dist/save-post.js","resume_policy":"cold_fallback","server_action":{"input":"form_data","response":"redirect","failure":"typed"}}}}"#,
584        )
585        .unwrap();
586        let mut contracts = SemanticPackageResolutionTable::default();
587        contracts.insert("post-service".into(), contract).unwrap();
588        let product = build_file_route_publication_v1(FileRoutePublicationRequestV1 {
589            configuration: crate::platform::WorkspaceConfiguration::default(),
590            sources: vec![ApplicationPublicationSourceV1 {
591                logical_path: "app/routes/posts/[slug].tsx".into(),
592                source: r#"
593import { savePost } from "post-service";
594@component() class Post {
595  @serverAction("savePost") save(): void {}
596  render() { return <form />; }
597}
598"#
599                .into(),
600            }],
601            package_contracts: contracts,
602            package_runtime_modules: SemanticPackageRuntimeModuleTable::default(),
603            profile: ApplicationPublicationProfileV1::Development,
604            output_root: "dist".into(),
605            environment_artifact: None,
606            v2_authoring: BTreeMap::new(),
607        })
608        .expect("route server-action handoff publication");
609
610        let plan = String::from_utf8(
611            product.artifacts[&PathBuf::from("route-server-actions.plan.json")].clone(),
612        )
613        .unwrap();
614        assert!(plan.contains("post-service"));
615        assert!(plan.contains("form_data"));
616        assert!(plan.contains("redirect"));
617        assert!(plan.contains("cold_fallback"));
618        assert!(!plan.contains("function savePost"));
619    }
620
621    #[test]
622    fn resolves_page_and_asset_paths_from_the_compiler_manifest() {
623        let manifest = FileRoutePublicationManifestV1 {
624            schema_version: 1,
625            compiler_contract: FILE_ROUTE_PUBLICATION_COMPILER_CONTRACT_V1.into(),
626            profile: "development".into(),
627            routes: vec![
628                FileRoutePublicationRouteV1 {
629                    path: "/posts/:slug".into(),
630                    entry_component_id: "component:post".into(),
631                    artifact_root: "routes/segment-posts/parameter-slug".into(),
632                    layout_component_ids: Vec::new(),
633                },
634                FileRoutePublicationRouteV1 {
635                    path: "/posts/new".into(),
636                    entry_component_id: "component:new".into(),
637                    artifact_root: "routes/segment-posts/segment-new".into(),
638                    layout_component_ids: Vec::new(),
639                },
640            ],
641            artifacts: Vec::new(),
642        };
643        assert_eq!(
644            resolve_file_route_request_v1(&manifest, "/posts/new"),
645            Some(FileRouteRequestTargetV1::Redirect {
646                location: "/posts/new/".into()
647            })
648        );
649        assert_eq!(
650            resolve_file_route_request_v1(&manifest, "/posts/hello/runtime.js"),
651            Some(FileRouteRequestTargetV1::Artifact {
652                path: "routes/segment-posts/parameter-slug/runtime.js".into()
653            })
654        );
655    }
656
657    #[test]
658    fn retains_dynamic_parameter_facts_from_the_selected_compiler_route() {
659        let manifest = FileRoutePublicationManifestV1 {
660            schema_version: 1,
661            compiler_contract: FILE_ROUTE_PUBLICATION_COMPILER_CONTRACT_V1.into(),
662            profile: "development".into(),
663            routes: vec![FileRoutePublicationRouteV1 {
664                path: "/posts/:slug/comments/:commentId".into(),
665                entry_component_id: "component:comment".into(),
666                artifact_root: "routes/comment".into(),
667                layout_component_ids: Vec::new(),
668            }],
669            artifacts: Vec::new(),
670        };
671
672        let resolved =
673            resolve_file_route_request_match_v1(&manifest, "/posts/hello-world/comments/42/")
674                .expect("selected route");
675        assert_eq!(resolved.route_path, "/posts/:slug/comments/:commentId");
676        assert_eq!(resolved.entry_component_id, "component:comment");
677        assert_eq!(
678            resolved.parameters,
679            BTreeMap::from([
680                ("slug".to_string(), "hello-world".to_string()),
681                ("commentId".to_string(), "42".to_string()),
682            ])
683        );
684        assert_eq!(
685            resolved.target,
686            FileRouteRequestTargetV1::Artifact {
687                path: "routes/comment/index.html".into(),
688            }
689        );
690    }
691}