1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use crate::compilation_unit::CompilationUnit;
5use crate::module_graph::{ModuleEdgeKind, ModuleGraph, ModuleTarget};
6use crate::semantic_package::{
7 SemanticPackageFormSubmission, SemanticPackageKind, SemanticPackageOpaqueTerminal,
8 SemanticPackagePureOperation, SemanticPackageResolutionTable, SemanticPackageResourceEndpoint,
9 SemanticPackageRouteLoader, SemanticPackageServerAction,
10};
11use crate::symbol_table::{ModuleSymbol, SymbolKind, SymbolTable};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct BindingTable {
16 pub modules: Vec<ModuleBindingTable>,
17 pub diagnostics: Vec<BindingDiagnostic>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ModuleBindingTable {
22 pub path: PathBuf,
23 pub exports: BTreeMap<String, ExportBinding>,
24 pub imports: BTreeMap<String, ImportBinding>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ExportBinding {
29 pub exported_name: String,
30 pub local_name: String,
31 pub symbol: ModuleSymbol,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ImportBinding {
36 pub local_name: String,
37 pub imported_name: String,
38 pub source_module: PathBuf,
39 pub target: ImportBindingTarget,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum ImportBindingTarget {
44 Symbol(ModuleSymbol),
45 Namespace {
46 module: PathBuf,
47 exports: BTreeMap<String, ModuleSymbol>,
48 },
49 SemanticPackage {
50 package: String,
51 version: String,
52 integrity: String,
53 export: String,
54 kind: SemanticPackageKind,
55 type_signature: String,
56 runtime_module: String,
57 resume_policy: String,
58 pure_operation: Option<SemanticPackagePureOperation>,
59 resource_endpoint: Option<SemanticPackageResourceEndpoint>,
60 route_loader: Option<SemanticPackageRouteLoader>,
61 server_action: Option<SemanticPackageServerAction>,
62 form_submission: Option<SemanticPackageFormSubmission>,
63 opaque_terminal: Option<SemanticPackageOpaqueTerminal>,
64 },
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct BindingDiagnostic {
69 pub code: String,
70 pub module: PathBuf,
71 pub name: String,
72 pub message: String,
73}
74
75impl BindingTable {
76 #[must_use]
77 pub fn module(&self, path: impl AsRef<Path>) -> Option<&ModuleBindingTable> {
78 self.modules
79 .iter()
80 .find(|module| module.path == path.as_ref())
81 }
82
83 #[must_use]
84 pub fn resolve_import(
85 &self,
86 module_path: impl AsRef<Path>,
87 local_name: &str,
88 ) -> Option<&ImportBinding> {
89 self.module(module_path)
90 .and_then(|module| module.imports.get(local_name))
91 }
92}
93
94#[must_use]
95pub fn build_binding_table(
96 unit: &CompilationUnit,
97 symbols: &SymbolTable,
98 modules: &ModuleGraph,
99) -> BindingTable {
100 build_binding_table_with_packages(
101 unit,
102 symbols,
103 modules,
104 &SemanticPackageResolutionTable::default(),
105 )
106}
107
108#[must_use]
109pub fn build_binding_table_with_packages(
110 unit: &CompilationUnit,
111 symbols: &SymbolTable,
112 modules: &ModuleGraph,
113 packages: &SemanticPackageResolutionTable,
114) -> BindingTable {
115 let mut tables = unit
116 .files()
117 .iter()
118 .map(|file| ModuleBindingTable {
119 path: file.path.clone(),
120 exports: BTreeMap::new(),
121 imports: BTreeMap::new(),
122 })
123 .collect::<Vec<_>>();
124 let mut diagnostics = Vec::new();
125
126 collect_local_exports(unit, symbols, &mut tables, &mut diagnostics);
127 resolve_relative_reexports(unit, modules, &mut tables, &mut diagnostics);
128 collect_identity_collisions(symbols, &mut diagnostics);
129
130 let exports_by_path = tables
131 .iter()
132 .map(|table| (table.path.clone(), table.exports.clone()))
133 .collect::<BTreeMap<_, _>>();
134
135 resolve_relative_imports(
136 unit,
137 modules,
138 &exports_by_path,
139 &mut tables,
140 &mut diagnostics,
141 packages,
142 );
143
144 BindingTable {
145 modules: tables,
146 diagnostics,
147 }
148}
149
150fn collect_local_exports(
151 unit: &CompilationUnit,
152 symbols: &SymbolTable,
153 tables: &mut [ModuleBindingTable],
154 diagnostics: &mut Vec<BindingDiagnostic>,
155) {
156 for (file, table) in unit.files().iter().zip(tables.iter_mut()) {
157 let Some(module_symbols) = symbols.module(&file.path) else {
158 continue;
159 };
160
161 for export in file.exports.iter().filter(|export| export.source.is_none()) {
162 for specifier in &export.specifiers {
163 let Some(local_name) = &specifier.local else {
164 diagnostics.push(BindingDiagnostic {
165 code: "PSBIND1001".to_string(),
166 module: file.path.clone(),
167 name: specifier.exported.clone(),
168 message: format!(
169 "export `{}` has no local declaration to bind",
170 specifier.exported
171 ),
172 });
173 continue;
174 };
175 let Some(symbol) = module_symbols.symbols.get(local_name) else {
176 diagnostics.push(BindingDiagnostic {
177 code: "PSBIND1001".to_string(),
178 module: file.path.clone(),
179 name: specifier.exported.clone(),
180 message: format!(
181 "export `{}` refers to unsupported or missing local symbol `{local_name}`",
182 specifier.exported
183 ),
184 });
185 continue;
186 };
187
188 insert_export(
189 table,
190 diagnostics,
191 ExportBinding {
192 exported_name: specifier.exported.clone(),
193 local_name: local_name.clone(),
194 symbol: symbol.clone(),
195 },
196 );
197 }
198 }
199 }
200}
201
202fn resolve_relative_imports(
203 unit: &CompilationUnit,
204 modules: &ModuleGraph,
205 exports_by_path: &BTreeMap<PathBuf, BTreeMap<String, ExportBinding>>,
206 tables: &mut [ModuleBindingTable],
207 diagnostics: &mut Vec<BindingDiagnostic>,
208 packages: &SemanticPackageResolutionTable,
209) {
210 for (file, table) in unit.files().iter().zip(tables.iter_mut()) {
211 for import in &file.imports {
212 let target = module_target(modules, &file.path, import.span, ModuleEdgeKind::Import);
213 if matches!(target, ModuleTarget::External) {
214 resolve_semantic_package_import(import, packages, table, diagnostics, &file.path);
215 continue;
216 }
217 let ModuleTarget::Resolved(target_path) = target else {
218 if matches!(target, ModuleTarget::Unresolved) {
219 diagnostics.push(BindingDiagnostic {
220 code: "PSBIND1002".to_string(),
221 module: file.path.clone(),
222 name: import.source.clone(),
223 message: format!(
224 "relative import `{}` does not resolve to a module",
225 import.source
226 ),
227 });
228 }
229 continue;
230 };
231 let target_exports = exports_by_path
232 .get(&target_path)
233 .cloned()
234 .unwrap_or_default();
235
236 for specifier in &import.specifiers {
237 let binding = resolve_import_binding(specifier, &target_path, &target_exports);
238
239 let Some(binding) = binding else {
240 diagnostics.push(BindingDiagnostic {
241 code: "PSBIND1003".to_string(),
242 module: file.path.clone(),
243 name: specifier.local.clone(),
244 message: format!(
245 "import `{}` is not exported by `{}`",
246 specifier.imported,
247 target_path.display()
248 ),
249 });
250 continue;
251 };
252
253 insert_import(table, diagnostics, binding);
254 }
255 }
256 }
257}
258
259fn resolve_semantic_package_import(
260 import: &presolve_parser::ParsedImport,
261 packages: &SemanticPackageResolutionTable,
262 table: &mut ModuleBindingTable,
263 diagnostics: &mut Vec<BindingDiagnostic>,
264 module: &Path,
265) {
266 if import.source == "presolve" {
271 return;
272 }
273 let Some(contract) = packages.contract(&import.source) else {
274 diagnostics.push(BindingDiagnostic {
275 code: "PSBIND1009".into(),
276 module: module.to_path_buf(),
277 name: import.source.clone(),
278 message: format!(
279 "external import `{}` has no semantic package contract",
280 import.source
281 ),
282 });
283 return;
284 };
285 for specifier in &import.specifiers {
286 let Some(export) = contract.exports.get(&specifier.imported) else {
287 diagnostics.push(BindingDiagnostic {
288 code: "PSBIND1010".into(),
289 module: module.to_path_buf(),
290 name: specifier.imported.clone(),
291 message: format!(
292 "semantic package `{}` does not declare export `{}`",
293 import.source, specifier.imported
294 ),
295 });
296 continue;
297 };
298 insert_import(
299 table,
300 diagnostics,
301 ImportBinding {
302 local_name: specifier.local.clone(),
303 imported_name: specifier.imported.clone(),
304 source_module: PathBuf::from(&import.source),
305 target: ImportBindingTarget::SemanticPackage {
306 package: contract.package.clone(),
307 version: contract.version.clone(),
308 integrity: contract.integrity.clone(),
309 export: specifier.imported.clone(),
310 kind: export.kind.clone(),
311 type_signature: export.type_signature.clone(),
312 runtime_module: export.runtime_module.clone(),
313 resume_policy: export.resume_policy.clone(),
314 pure_operation: export.pure_operation,
315 resource_endpoint: export.resource_endpoint.clone(),
316 route_loader: export.route_loader.clone(),
317 server_action: export.server_action.clone(),
318 form_submission: export.form_submission.clone(),
319 opaque_terminal: export.opaque_terminal.clone(),
320 },
321 },
322 );
323 }
324}
325
326fn resolve_relative_reexports(
327 unit: &CompilationUnit,
328 modules: &ModuleGraph,
329 tables: &mut [ModuleBindingTable],
330 diagnostics: &mut Vec<BindingDiagnostic>,
331) {
332 for _ in 0..unit.files().len() {
333 let exports_by_path = tables
334 .iter()
335 .map(|table| (table.path.clone(), table.exports.clone()))
336 .collect::<BTreeMap<_, _>>();
337 let mut changed = false;
338
339 for (file, table) in unit.files().iter().zip(tables.iter_mut()) {
340 for export in file.exports.iter().filter(|export| export.source.is_some()) {
341 let edge_kind = match export.kind {
342 presolve_parser::ParsedExportKind::Named => ModuleEdgeKind::NamedReExport,
343 presolve_parser::ParsedExportKind::All => ModuleEdgeKind::ExportAll,
344 presolve_parser::ParsedExportKind::Default => continue,
345 };
346 let target = module_target(modules, &file.path, export.span, edge_kind);
347 let ModuleTarget::Resolved(target_path) = target else {
348 if matches!(target, ModuleTarget::Unresolved) {
349 push_diagnostic_once(
350 diagnostics,
351 BindingDiagnostic {
352 code: "PSBIND1006".to_string(),
353 module: file.path.clone(),
354 name: export.source.clone().unwrap_or_default(),
355 message: "relative re-export does not resolve to a module"
356 .to_string(),
357 },
358 );
359 }
360 continue;
361 };
362 let target_exports = exports_by_path
363 .get(&target_path)
364 .cloned()
365 .unwrap_or_default();
366
367 match export.kind {
368 presolve_parser::ParsedExportKind::Named => {
369 for specifier in &export.specifiers {
370 let Some(local_name) = &specifier.local else {
371 continue;
372 };
373 let Some(target) = target_exports.get(local_name) else {
374 continue;
375 };
376 changed |= insert_reexport(
377 table,
378 ExportBinding {
379 exported_name: specifier.exported.clone(),
380 local_name: local_name.clone(),
381 symbol: target.symbol.clone(),
382 },
383 );
384 }
385 }
386 presolve_parser::ParsedExportKind::All if export.specifiers.is_empty() => {
387 for (name, target) in target_exports {
388 if name != "default" {
389 changed |= insert_reexport(
390 table,
391 ExportBinding {
392 exported_name: name.clone(),
393 local_name: target.local_name,
394 symbol: target.symbol,
395 },
396 );
397 }
398 }
399 }
400 presolve_parser::ParsedExportKind::All => {
401 push_diagnostic_once(
402 diagnostics,
403 BindingDiagnostic {
404 code: "PSBIND1007".to_string(),
405 module: file.path.clone(),
406 name: export.source.clone().unwrap_or_default(),
407 message: "namespace re-export bindings are not supported yet"
408 .to_string(),
409 },
410 );
411 }
412 presolve_parser::ParsedExportKind::Default => {}
413 }
414 }
415 }
416
417 if !changed {
418 break;
419 }
420 }
421}
422
423fn insert_reexport(table: &mut ModuleBindingTable, binding: ExportBinding) -> bool {
424 if table.exports.contains_key(&binding.exported_name) {
425 return false;
426 }
427
428 table.exports.insert(binding.exported_name.clone(), binding);
429 true
430}
431
432fn collect_identity_collisions(symbols: &SymbolTable, diagnostics: &mut Vec<BindingDiagnostic>) {
433 let mut paths_by_id = BTreeMap::<String, Vec<PathBuf>>::new();
434
435 for module in &symbols.modules {
436 for symbol in module
437 .symbols
438 .values()
439 .filter(|symbol| symbol.kind == SymbolKind::Component)
440 {
441 paths_by_id
442 .entry(symbol.id.as_str().to_string())
443 .or_default()
444 .push(module.path.clone());
445 }
446 }
447
448 for paths in paths_by_id.values_mut() {
449 paths.sort();
450 paths.dedup();
451 }
452
453 for (id, paths) in paths_by_id.into_iter().filter(|(_, paths)| paths.len() > 1) {
454 diagnostics.push(BindingDiagnostic {
455 code: "PSBIND1008".to_string(),
456 module: paths[0].clone(),
457 name: id.clone(),
458 message: format!(
459 "semantic identity `{id}` collides across modules: {}",
460 paths
461 .iter()
462 .map(|path| path.display().to_string())
463 .collect::<Vec<_>>()
464 .join(", ")
465 ),
466 });
467 }
468}
469
470fn push_diagnostic_once(diagnostics: &mut Vec<BindingDiagnostic>, diagnostic: BindingDiagnostic) {
471 if diagnostics.iter().any(|existing| existing == &diagnostic) {
472 return;
473 }
474
475 diagnostics.push(diagnostic);
476}
477
478fn resolve_import_binding(
479 specifier: &presolve_parser::ParsedImportSpecifier,
480 target_path: &Path,
481 target_exports: &BTreeMap<String, ExportBinding>,
482) -> Option<ImportBinding> {
483 if specifier.imported == "*" {
484 return Some(ImportBinding {
485 local_name: specifier.local.clone(),
486 imported_name: specifier.imported.clone(),
487 source_module: target_path.to_path_buf(),
488 target: ImportBindingTarget::Namespace {
489 module: target_path.to_path_buf(),
490 exports: target_exports
491 .iter()
492 .map(|(name, export)| (name.clone(), export.symbol.clone()))
493 .collect(),
494 },
495 });
496 }
497
498 target_exports
499 .get(&specifier.imported)
500 .map(|export| ImportBinding {
501 local_name: specifier.local.clone(),
502 imported_name: specifier.imported.clone(),
503 source_module: target_path.to_path_buf(),
504 target: ImportBindingTarget::Symbol(export.symbol.clone()),
505 })
506}
507
508fn module_target(
509 modules: &ModuleGraph,
510 source: &Path,
511 span: presolve_parser::SourceSpan,
512 kind: ModuleEdgeKind,
513) -> ModuleTarget {
514 modules
515 .edges
516 .iter()
517 .find(|edge| edge.kind == kind && edge.source == source && edge.span == span)
518 .map_or(ModuleTarget::Unresolved, |edge| edge.target.clone())
519}
520
521fn insert_export(
522 table: &mut ModuleBindingTable,
523 diagnostics: &mut Vec<BindingDiagnostic>,
524 binding: ExportBinding,
525) {
526 if table.exports.contains_key(&binding.exported_name) {
527 diagnostics.push(BindingDiagnostic {
528 code: "PSBIND1004".to_string(),
529 module: table.path.clone(),
530 name: binding.exported_name.clone(),
531 message: format!("duplicate export binding `{}`", binding.exported_name),
532 });
533 return;
534 }
535
536 table.exports.insert(binding.exported_name.clone(), binding);
537}
538
539fn insert_import(
540 table: &mut ModuleBindingTable,
541 diagnostics: &mut Vec<BindingDiagnostic>,
542 binding: ImportBinding,
543) {
544 if table.imports.contains_key(&binding.local_name) {
545 diagnostics.push(BindingDiagnostic {
546 code: "PSBIND1005".to_string(),
547 module: table.path.clone(),
548 name: binding.local_name.clone(),
549 message: format!("duplicate import binding `{}`", binding.local_name),
550 });
551 return;
552 }
553
554 table.imports.insert(binding.local_name.clone(), binding);
555}
556
557#[cfg(test)]
558mod tests {
559 use super::{build_binding_table, build_binding_table_with_packages, ImportBindingTarget};
560 use crate::semantic_package::{
561 parse_semantic_package_contract, SemanticPackageResolutionTable,
562 };
563 use crate::{build_module_graph, build_symbol_table, CompilationUnit, SemanticId};
564
565 #[test]
566 fn resolves_relative_named_default_and_namespace_imports() {
567 let unit = CompilationUnit::parse_sources([
568 (
569 "src/app/App.tsx",
570 r#"
571import { Widget as CounterWidget } from "../ui/Counter";
572import StatusCard from "../ui/Status";
573import * as counter from "../ui/Counter";
574import { packageValue } from "presolve-runtime";
575"#,
576 ),
577 (
578 "src/ui/Counter.tsx",
579 r#"
580@component("x-counter")
581export class Counter extends Component {
582 render() {
583 return <div>Counter</div>;
584 }
585}
586
587export { Counter as Widget };
588"#,
589 ),
590 (
591 "src/ui/Status.tsx",
592 r#"
593@component("x-status")
594export default class Status extends Component {
595 render() {
596 return <div>Status</div>;
597 }
598}
599"#,
600 ),
601 ]);
602 let symbols = build_symbol_table(&unit);
603 let modules = build_module_graph(&unit);
604 let bindings = build_binding_table(&unit, &symbols, &modules);
605 let app = bindings.module("src/app/App.tsx").expect("app module");
606
607 assert_eq!(
608 bindings
609 .diagnostics
610 .iter()
611 .map(|diagnostic| diagnostic.code.as_str())
612 .collect::<Vec<_>>(),
613 vec!["PSBIND1009"]
614 );
615 assert_eq!(app.imports.len(), 3);
616 assert_eq!(app.exports.len(), 0);
617 assert_eq!(
618 app.imports["CounterWidget"].target,
619 ImportBindingTarget::Symbol(
620 bindings
621 .module("src/ui/Counter.tsx")
622 .expect("counter module")
623 .exports["Widget"]
624 .symbol
625 .clone()
626 )
627 );
628 assert_eq!(
629 app.imports["StatusCard"].target,
630 ImportBindingTarget::Symbol(
631 bindings
632 .module("src/ui/Status.tsx")
633 .expect("status module")
634 .exports["default"]
635 .symbol
636 .clone()
637 )
638 );
639
640 let ImportBindingTarget::Namespace { module, exports } = &app.imports["counter"].target
641 else {
642 panic!("expected namespace binding");
643 };
644 assert_eq!(module, &std::path::PathBuf::from("src/ui/Counter.tsx"));
645 assert_eq!(
646 exports["Widget"].id,
647 SemanticId::component_in_module("src/ui/Counter.tsx", Some("x-counter"), "Counter")
648 );
649 assert!(bindings
650 .resolve_import("src/app/App.tsx", "packageValue")
651 .is_none());
652 }
653
654 #[test]
655 fn reports_unresolved_relative_and_missing_export_bindings() {
656 let unit = CompilationUnit::parse_sources([
657 (
658 "src/App.tsx",
659 r#"
660import { Missing } from "./Counter";
661import { MissingModule } from "./missing";
662"#,
663 ),
664 (
665 "src/Counter.tsx",
666 r#"
667@component("x-counter")
668export class Counter extends Component {
669 render() {
670 return <div>Counter</div>;
671 }
672}
673"#,
674 ),
675 ]);
676 let symbols = build_symbol_table(&unit);
677 let modules = build_module_graph(&unit);
678 let bindings = build_binding_table(&unit, &symbols, &modules);
679
680 let codes = bindings
681 .diagnostics
682 .iter()
683 .map(|diagnostic| diagnostic.code.as_str())
684 .collect::<Vec<_>>();
685
686 assert_eq!(codes, vec!["PSBIND1003", "PSBIND1002"]);
687 }
688
689 #[test]
690 fn resolves_named_and_export_all_reexport_chains() {
691 let unit = CompilationUnit::parse_sources([
692 (
693 "src/App.tsx",
694 r#"
695import { PrimaryButton } from "./index";
696"#,
697 ),
698 (
699 "src/base.tsx",
700 r#"
701@component("x-button")
702export class Button extends Component {
703 render() {
704 return <button>Button</button>;
705 }
706}
707"#,
708 ),
709 (
710 "src/bridge.ts",
711 r#"
712export { Button as PrimaryButton } from "./base";
713"#,
714 ),
715 (
716 "src/index.ts",
717 r#"
718export * from "./bridge";
719"#,
720 ),
721 ]);
722 let symbols = build_symbol_table(&unit);
723 let modules = build_module_graph(&unit);
724 let bindings = build_binding_table(&unit, &symbols, &modules);
725
726 assert!(bindings.diagnostics.is_empty());
727 assert_eq!(
728 bindings
729 .resolve_import("src/App.tsx", "PrimaryButton")
730 .expect("resolved re-export")
731 .target,
732 ImportBindingTarget::Symbol(
733 bindings
734 .module("src/index.ts")
735 .expect("index module")
736 .exports["PrimaryButton"]
737 .symbol
738 .clone()
739 )
740 );
741 }
742
743 #[test]
744 fn module_qualified_component_ids_do_not_collide_across_modules() {
745 let unit = CompilationUnit::parse_sources([
746 (
747 "src/First.tsx",
748 r#"
749@component("x-duplicate")
750class First extends Component {
751 render() {
752 return <div>First</div>;
753 }
754}
755"#,
756 ),
757 (
758 "src/Second.tsx",
759 r#"
760@component("x-duplicate")
761class Second extends Component {
762 render() {
763 return <div>Second</div>;
764 }
765}
766"#,
767 ),
768 ]);
769 let symbols = build_symbol_table(&unit);
770 let modules = build_module_graph(&unit);
771 let bindings = build_binding_table(&unit, &symbols, &modules);
772
773 assert!(bindings.diagnostics.is_empty());
774 assert_ne!(
775 symbols
776 .module("src/First.tsx")
777 .expect("first module")
778 .symbols["First"]
779 .id,
780 symbols
781 .module("src/Second.tsx")
782 .expect("second module")
783 .symbols["Second"]
784 .id
785 );
786 }
787
788 #[test]
789 fn resolves_external_imports_only_through_semantic_package_contracts() {
790 let unit = CompilationUnit::parse_sources([(
791 "src/App.tsx",
792 "import { format } from \"date-kit\"; export class App {}",
793 )]);
794 let symbols = build_symbol_table(&unit);
795 let modules = build_module_graph(&unit);
796 let contract = parse_semantic_package_contract(r#"{"schema_version":1,"package":"date-kit","version":"1.0.0","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"format":{"kind":"pure","type_signature":"(Date)->string","runtime_module":"dist/format.js","resume_policy":"input_only"}}}"#).unwrap();
797 let mut packages = SemanticPackageResolutionTable::default();
798 packages.insert("date-kit".into(), contract).unwrap();
799 let bindings = build_binding_table_with_packages(&unit, &symbols, &modules, &packages);
800 assert!(bindings.diagnostics.is_empty());
801 let ImportBindingTarget::SemanticPackage {
802 package,
803 version,
804 integrity,
805 export,
806 kind,
807 type_signature,
808 runtime_module,
809 resume_policy,
810 pure_operation: _,
811 resource_endpoint: _,
812 route_loader: _,
813 server_action: _,
814 form_submission: _,
815 opaque_terminal: _,
816 } = &bindings
817 .resolve_import("src/App.tsx", "format")
818 .unwrap()
819 .target
820 else {
821 panic!("expected semantic package binding");
822 };
823 assert_eq!(package, "date-kit");
824 assert_eq!(version, "1.0.0");
825 assert_eq!(
826 integrity,
827 "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
828 );
829 assert_eq!(export, "format");
830 assert_eq!(kind, &crate::semantic_package::SemanticPackageKind::Pure);
831 assert_eq!(type_signature, "(Date)->string");
832 assert_eq!(runtime_module, "dist/format.js");
833 assert_eq!(resume_policy, "input_only");
834 }
835
836 #[test]
837 fn accepts_the_public_presolve_authoring_import_without_a_package_capability_contract() {
838 let unit = CompilationUnit::parse_sources([(
839 "src/App.tsx",
840 "import { component, state } from \"presolve\"; export class App {}",
841 )]);
842 let symbols = build_symbol_table(&unit);
843 let modules = build_module_graph(&unit);
844 let bindings = build_binding_table(&unit, &symbols, &modules);
845 assert!(bindings.diagnostics.is_empty());
846 assert!(bindings.module("src/App.tsx").unwrap().imports.is_empty());
847 }
848
849 #[test]
850 fn rejects_external_imports_without_a_matching_contract_or_export() {
851 let unit = CompilationUnit::parse_sources([(
852 "src/App.tsx",
853 "import present from \"date-kit\"; import { absent } from \"date-kit\"; import { other } from \"other-kit\"; export class App {}",
854 )]);
855 let symbols = build_symbol_table(&unit);
856 let modules = build_module_graph(&unit);
857 let contract = parse_semantic_package_contract(r#"{"schema_version":1,"package":"date-kit","version":"1.0.0","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"default":{"kind":"pure","type_signature":"(Date)->string","runtime_module":"dist/default.js","resume_policy":"input_only"}}}"#).unwrap();
858 let mut packages = SemanticPackageResolutionTable::default();
859 packages.insert("date-kit".into(), contract).unwrap();
860
861 let bindings = build_binding_table_with_packages(&unit, &symbols, &modules, &packages);
862 assert_eq!(
863 bindings
864 .diagnostics
865 .iter()
866 .map(|diagnostic| diagnostic.code.as_str())
867 .collect::<Vec<_>>(),
868 vec!["PSBIND1010", "PSBIND1009"]
869 );
870 assert!(bindings.resolve_import("src/App.tsx", "present").is_some());
871 assert!(bindings.resolve_import("src/App.tsx", "absent").is_none());
872 assert!(bindings.resolve_import("src/App.tsx", "other").is_none());
873 }
874}