1pub mod application_publication;
8pub mod application_semantic_model;
9pub mod asm_validation;
10pub mod binding_table;
11pub mod compilation_unit;
12pub mod compiler_pass;
13pub mod component_composition;
14pub mod component_diagnostics;
15pub mod component_graph;
16pub mod component_initialization;
17pub mod component_instance;
18pub mod component_instance_scope;
19pub mod component_invocation;
20pub mod component_ir;
21pub mod component_ir_optimization;
22pub mod component_scope;
23pub mod composition_typing;
24pub mod computed_instance_slots;
25pub mod computed_value;
26pub mod consumer;
27pub mod context;
28pub mod context_declaration_candidate;
29pub mod context_dependency;
30mod context_designator;
31pub mod context_diagnostics;
32pub mod context_evaluation;
33pub mod context_inspection;
34pub mod context_lifetime;
35pub mod context_ownership;
36pub mod context_resolution;
37pub mod context_resume;
38pub mod context_typing;
39pub mod context_update;
40pub mod effect;
41pub mod effect_capability;
42pub mod effect_diagnostics;
43pub mod effect_inspection;
44pub mod effect_resume;
45pub mod explain;
46pub mod expression_graph;
47pub mod file_route_publication;
48pub mod form;
49pub mod form_binding;
50pub mod form_diagnostics;
51pub mod form_field;
52pub mod form_inspection;
53pub mod form_ir;
54pub mod form_ir_optimization;
55pub mod form_ownership;
56pub mod form_reset;
57pub mod form_serialization;
58pub mod form_submission;
59pub mod form_submission_host;
60pub mod form_tracking;
61pub mod form_validation;
62pub mod form_validation_plan;
63pub mod html_codegen;
64pub mod instance_context;
65pub mod intermediate_representation;
66#[cfg(any(test, all(feature = "wasm", target_arch = "wasm32")))]
67mod language_service;
68pub mod layout_composition;
69pub mod layout_graph;
70pub mod lazy_action_chunks;
71pub mod metaframework_handoff;
72pub mod model;
73pub mod module_graph;
74pub mod opaque;
75pub mod ordinary_html_codegen;
76pub mod ordinary_template_instance;
77pub mod ordinary_template_integrity;
78pub mod page_codegen;
79pub mod persistent_cache;
80pub mod platform;
81pub mod production_audit;
82pub mod production_bootstrap;
83pub mod production_chunk_graph;
84pub mod production_cleanup;
85pub mod production_constant_pool;
86pub mod production_deduplication;
87pub mod production_diagnostics;
88pub mod production_elimination;
89pub mod production_module_emitter;
90pub mod production_optimization;
91pub mod production_reachability;
92pub mod production_reports;
93pub mod production_runtime_artifact;
94pub mod production_scheduler;
95pub mod production_validation;
96pub mod project_discovery;
97pub mod provider;
98pub mod resource;
99pub mod resume_activation;
100pub mod resume_anchor;
101pub mod resume_boot;
102pub mod resume_boundary;
103pub mod resume_capture;
104pub mod resume_chunk;
105pub mod resume_diagnostics;
106pub mod resume_explain;
107pub mod resume_identity;
108pub mod resume_instance;
109pub mod resume_liveness;
110pub mod resume_manifest;
111pub mod resume_plan;
112pub mod resume_restore;
113pub mod resume_schema;
114pub mod route_graph;
115pub mod route_loader;
116pub mod route_server_action;
117pub mod runtime_codegen;
118pub mod runtime_component;
119pub mod runtime_component_artifact;
120pub mod runtime_computed;
121pub mod runtime_computed_artifact;
122pub mod runtime_context;
123pub mod runtime_context_artifact;
124pub mod runtime_effect;
125pub mod runtime_effect_artifact;
126pub mod runtime_form_artifact;
127pub mod runtime_form_registry;
128pub mod runtime_opaque_artifact;
129pub mod runtime_resource_artifact;
130pub mod semantic_capability;
131pub mod semantic_graph;
132pub mod semantic_id;
133pub mod semantic_package;
134pub mod semantic_package_runtime;
135pub mod semantic_provenance;
136pub mod semantic_reference;
137pub mod semantic_type;
138pub mod service;
139pub mod shared_chunk_candidate;
140pub mod slot;
141pub mod slot_binding;
142pub mod slot_content;
143pub mod state_instance_storage;
144pub mod summarize;
145pub mod symbol_table;
146pub mod template_graph;
147pub mod template_manifest;
148pub mod template_semantics;
149pub mod tooling_products;
150pub mod tooling_reader;
151pub mod tooling_schema;
152pub use tooling_products::{
153 build_tooling_artifact_graph_v1, build_tooling_build_trace_v1,
154 build_tooling_compile_cost_report_v1, decode_tooling_artifact_graph_v1,
155 decode_tooling_build_trace_v1, decode_tooling_compile_cost_report_v1,
156 decode_tooling_query_snapshot_v1, tooling_artifact_graph_json_v1, tooling_build_trace_json_v1,
157 tooling_compile_cost_report_json_v1, tooling_query_snapshot_json_v1, ToolingArtifactGraphV1,
158 ToolingBuildTraceStageV1, ToolingBuildTraceV1, ToolingCompileCostReportV1,
159 ToolingProductValidationErrorV1, ToolingQueryDiagnosticSecondaryV1,
160 ToolingQueryDiagnosticSeverityV1, ToolingQueryDiagnosticV1, ToolingQueryRangeV1,
161 ToolingQueryReferenceKindV1, ToolingQueryReferenceV1, ToolingQuerySemanticKindV1,
162 ToolingQuerySemanticRecordV1, ToolingQuerySnapshotSourceUnitV1, ToolingQuerySnapshotV1,
163 ToolingTraceIdentityV1, ToolingTraceOutcomeV1, ToolingTraceStageKindV1,
164 ARTIFACT_GRAPH_TOOLING_SCHEMA_V1, BUILD_TRACE_TOOLING_SCHEMA_V1,
165 COMPILE_COST_TOOLING_SCHEMA_V1, QUERY_SNAPSHOT_TOOLING_SCHEMA_V1,
166};
167pub mod watch;
168pub mod workspace;
169
170#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
171mod wasm_binding;
172
173pub use application_publication::{
174 application_publication_manifest_json_v1, build_application_publication_product_from_asm_v1,
175 build_application_publication_product_v1, validate_application_publication_request_v1,
176 ApplicationPublicationArtifactV1, ApplicationPublicationErrorV1,
177 ApplicationPublicationManifestV1, ApplicationPublicationProductV1,
178 ApplicationPublicationProfileV1, ApplicationPublicationRequestErrorV1,
179 ApplicationPublicationRequestV1, ApplicationPublicationSourceV1,
180 ValidatedApplicationPublicationRequestV1, APPLICATION_PUBLICATION_COMPILER_CONTRACT_V1,
181 APPLICATION_PUBLICATION_MANIFEST_SCHEMA_VERSION,
182};
183pub use application_semantic_model::{
184 build_application_semantic_model, build_application_semantic_model_for_unit,
185 build_application_semantic_model_for_unit_with_packages,
186 build_application_semantic_model_from_component_graph,
187 build_file_route_application_semantic_model_for_route_with_packages,
188 build_file_route_application_semantic_model_for_unit_with_packages, ApplicationSemanticModel,
189 FileRouteApplicationModelErrorV1, SemanticEntity, SemanticEntityKind,
190};
191pub use asm_validation::{validate_application_semantic_model, AsmValidationDiagnostic};
192pub use binding_table::{
193 build_binding_table, build_binding_table_with_packages, BindingDiagnostic, BindingTable,
194 ExportBinding, ImportBinding, ImportBindingTarget, ModuleBindingTable,
195};
196pub use compilation_unit::CompilationUnit;
197pub use compiler_pass::{
198 fold_component_graph, AnalysisPass, ConstantEvaluation, ConstantEvaluationPass,
199 ConstantFoldingPass, DependencyAnalysis, DependencyAnalysisPass, ImmutableAsmPass,
200};
201pub use component_composition::{
202 analyze_component_composition, ComponentCompositionAnalysis, ComponentCompositionCycle,
203};
204pub use component_diagnostics::{
205 collect_component_diagnostics, ComponentDiagnosticContract, COMPONENT_DIAGNOSTIC_CONTRACTS,
206};
207pub use component_graph::{
208 build_component_graph, build_component_graph_for_module, is_valid_opaque_action_fact,
209 ArithmeticEvaluationError, ArithmeticExpression, ArithmeticExpressionKind, ArithmeticOperator,
210 AuthoredComponentHeritage, AuthoredContextDeclarationCandidate, AuthoredDeclarationKind,
211 AuthoredOpaqueActionFact, AuthoredRouteLoaderDeclarationFact, AuthoredServerActionFact,
212 AuthoredSlotDeclarationCandidate, AuthoredSubmissionDeclarationFact,
213 AuthoredValidationRuleArgument, AuthoredValidationRuleArgumentKind,
214 AuthoredValidationRuleDeclarationFact, AuthoredValidationRuleExpression,
215 AuthoredValidationRuleExpressionKind, ComparisonOperator, ComponentAction, ComponentDiagnostic,
216 ComponentDiagnosticSeverity, ComponentGraph, ComponentMethod, ComponentNode,
217 ComputedExpression, ComputedExpressionKind, ConstantEvaluationError, ConstantExpression,
218 ConstantExpressionKind, ConsumerDeclaration, ContextDeclaration,
219 ContextDeclarationCandidateKind, ContextDeclarationViolation, ContextDesignator,
220 DeclaredStateType, DeclaredStateTypeKind, DiagnosticSecondaryLabel, EffectBodySyntax,
221 EffectExpression, EffectExpressionKind, EffectStatementSyntax, EffectStatementSyntaxKind,
222 FormDeclarationCandidate, FormDeclarationStatus, FormDeclarationViolation, FormDesignatorFact,
223 FormFieldDeclarationCandidate, FormFieldDeclarationViolation, LogicalOperator, MethodCall,
224 MethodLocalVariable, MethodParameter, RenderAttribute, RenderAttributeValue, RenderChild,
225 RenderEventHandler, RenderFragment, RenderList, RenderModel, SerializableValue,
226 SlotDeclaration, SlotDeclarationViolation, SlotKind, StateField, StateOperation,
227 UnsupportedEffectStatementKind, UnsupportedFormDesignatorFact,
228};
229pub use component_initialization::{
230 plan_component_initialization, ComponentInitializationPlan, ComponentInstanceBatch,
231 SlotBindingBatch,
232};
233pub use component_instance::{
234 plan_component_instances, plan_component_instances_with_virtual_invocations,
235 BlockedComponentInstancePlan, BlockedComponentInstanceReason, ComponentBuildRoot,
236 ComponentBuildRootKind, ComponentInstance, ComponentInstancePlan, ComponentInstanceStatus,
237};
238pub use component_instance_scope::{
239 build_component_instance_scope_graph, validate_component_instance_scope_graph,
240 ComponentInstanceScopeDiagnostic, ComponentInstanceScopeGraph, ComponentInstanceScopeNode,
241 ComponentInstanceScopeViolation,
242};
243pub use component_invocation::{
244 collect_component_invocations, ComponentInvocationEntity, ComponentInvocationResolutionStatus,
245};
246pub use component_ir::{
247 lower_component_ir, validate_component_ir, ComponentIrInstruction, ComponentIrOperation,
248 ComponentIrReport,
249};
250pub use component_ir_optimization::{
251 optimize_component_ir, validate_optimized_component_ir, OptimizedComponentIrReport,
252};
253pub use component_scope::{ComponentScopeDiagnostic, ComponentScopeGraph};
254pub use composition_typing::{
255 collect_composition_type_products, ComponentInvocationTypeRecord, CompositionCompatibility,
256 CompositionTypeProducts, InstanceContextBindingTypeRecord, SlotBindingTypeRecord,
257};
258pub use computed_instance_slots::{
259 build_computed_instance_slot_registry, validate_computed_instance_slot_registry,
260 ComputedInstanceSlotRecord, ComputedInstanceSlotRegistry,
261 COMPUTED_INSTANCE_SLOT_REGISTRY_VERSION,
262};
263pub use computed_value::{
264 collect_computed_values, ComputedCachePolicy, ComputedDiagnosticCode, ComputedPurity,
265 ComputedPurityViolation, ComputedPurityViolationKind, ComputedValue,
266};
267pub use consumer::{collect_consumer_entities, ConsumerEntity, ContextResolutionState};
268pub use context::{collect_context_entities, ContextEntity};
269pub use context_declaration_candidate::{
270 collect_context_declaration_candidates, ContextDeclarationCandidate,
271 ContextDeclarationCandidateRegistry, ContextDeclarationStatus, ContextSemanticEntityId,
272};
273pub use context_dependency::{
274 collect_context_dependency_graph, ContextDependencyCompatibility, ContextDependencyEdge,
275 ContextDependencyEdgeKind, ContextDependencyGraph, ContextDependencyNode,
276 ContextDependencyNodeId, ContextDependencyNodeKind,
277};
278pub use context_diagnostics::collect_context_diagnostics;
279pub use context_evaluation::{
280 collect_context_evaluation_plan, ContextConsumerAvailabilityEntry,
281 ContextConsumerAvailabilityStatus, ContextEvaluationBatch, ContextEvaluationBatchId,
282 ContextEvaluationPlan, ContextEvaluationPlanId, ContextSourceBlockReason,
283 ContextSourcePlanEntry, ContextSourcePlanStatus, ContextValueSourceId,
284};
285pub use context_inspection::{
286 build_context_inspection_registry, ContextInspection, ContextInspectionRegistry,
287};
288pub use context_lifetime::{
289 collect_context_lifetime_analysis, ContextBindingLifetimeRecord, ContextBindingLifetimeSource,
290 ContextBindingLifetimeStatus, ContextDefaultLifetimeRecord, ContextDependencyLifetimeRecord,
291 ContextEntityLifetimeRecord, ContextLifetimeAnalysis, ContextLifetimeEntityId,
292 ContextLifetimeId, LifetimeCompatibilityStatus, ProviderLifetimeRecord,
293};
294pub use context_ownership::{
295 collect_context_ownership_graph, ContextOwnedEntities, ContextOwnershipEdge,
296 ContextOwnershipEdgeKind, ContextOwnershipGraph, ContextOwnershipNode, ContextOwnershipNodeId,
297 ContextOwnershipNodeKind, ContextOwnershipOwnerId, ContextOwnershipTargetId,
298};
299pub use context_resolution::{
300 collect_context_resolutions, ContextResolution, ContextResolutionResult,
301};
302pub use context_resume::{
303 build_context_resume_plan, ContextResumePlan, ContextResumeRecord, ContextResumeSlotId,
304 ContextSlotResumeStatus,
305};
306pub use context_typing::{
307 collect_context_type_products, CompatibilityStatus, ConsumerTypeRecord,
308 ContextBindingCompatibility, ContextBindingTypeRecord, ContextSerializationCompatibility,
309 ContextTypeProducts, ContextTypeRecord, ProviderTypeRecord,
310};
311pub use context_update::{build_context_update_plan, ContextActionUpdatePlan, ContextUpdatePlan};
312pub use effect::{
313 analyze_effect_reactivity, collect_effects, derive_effect_trigger_plan, lower_effect_bodies,
314 plan_effect_execution, validate_effects, ActionBatch, ActionBatchEffectTrigger,
315 ActionEffectExecutionPlan, Effect, EffectBody, EffectComputedPrerequisiteBatch,
316 EffectExecutionBatch, EffectExecutionPlan, EffectExecutionPolicy, EffectReactiveAnalysis,
317 EffectRenderBoundary, EffectSemanticViolation, EffectSemanticViolationKind, EffectStatement,
318 EffectStatementKind, EffectTriggerPlan, EffectValidation, InitialEffectExecutionPlan,
319 UnplannedEffect, UnplannedEffectReason,
320};
321pub use effect_capability::{
322 ArgumentSerializationPolicy, BuiltinCapabilityProvenance, CapabilityDefinition, CapabilityId,
323 CapabilityOperation, CapabilityOperationId, CapabilityOperationKind, CapabilityParameters,
324 CapabilityResultPolicy, CapabilitySignature, CapabilityValueContract, EffectCapabilityRegistry,
325 RuntimeCapabilityLowering, StaticCapabilityPath, EFFECT_CAPABILITY_REGISTRY,
326 EFFECT_CAPABILITY_REGISTRY_VERSION,
327};
328pub use effect_diagnostics::{collect_effect_diagnostics, EffectDiagnosticCode};
329pub use effect_inspection::{
330 build_effect_inspection_registry, validate_effect_inspection_registry, EffectInspection,
331 EffectInspectionActionTrigger, EffectInspectionCapability, EffectInspectionDependencies,
332 EffectInspectionInitialTrigger, EffectInspectionIr, EffectInspectionPrerequisiteBatch,
333 EffectInspectionProvenance, EffectInspectionRegistry, EffectInspectionResumability,
334 EffectInspectionRuntime, EffectInspectionSchedule, EffectInspectionScheduledAction,
335 EffectInspectionUnplanned, EffectInspectionValidation, EffectInspectionValidationDiagnostic,
336 EffectInspectionViolation,
337};
338pub use effect_resume::{
339 build_effect_resume_plan, validate_effect_resume_plan, EffectActivationSlotId,
340 EffectActivationStatus, EffectInitialResumeMembership, EffectResumePlan, EffectResumeRecord,
341 EffectResumeValidationDiagnostic,
342};
343pub use explain::{explain_json, explain_text};
344pub use expression_graph::{ExpressionGraph, ExpressionNode, ExpressionNodeKind};
345pub use file_route_publication::{
346 build_file_route_publication_v1, file_route_artifact_root_v1,
347 file_route_publication_manifest_json_v1, resolve_file_route_request_match_v1,
348 resolve_file_route_request_v1, FileRoutePublicationErrorV1, FileRoutePublicationManifestV1,
349 FileRoutePublicationProductV1, FileRoutePublicationRequestV1, FileRoutePublicationRouteV1,
350 FileRouteRequestMatchV1, FileRouteRequestTargetV1, FILE_ROUTE_PUBLICATION_COMPILER_CONTRACT_V1,
351 FILE_ROUTE_PUBLICATION_MANIFEST_SCHEMA_VERSION,
352};
353pub use form::{collect_form_entities, FormEntity};
354pub use form_binding::{
355 collect_form_field_binding_products, FormControlChannel, FormControlCompatibility,
356 FormControlNormalization, FormFieldBinding, FormFieldBindingCandidate,
357 FormFieldBindingEvidence, FormFieldBindingEvidenceKind, FormFieldBindingExpressionFact,
358 FormFieldBindingProducts, FormFieldBindingViolation, FormInputKind,
359};
360pub use form_diagnostics::{
361 collect_form_diagnostics, FormDiagnosticReservation, FORM_DIAGNOSTIC_RESERVATIONS,
362};
363pub use form_field::{collect_form_field_products, FormFieldEntity, FormFieldProducts};
364pub use form_inspection::{build_form_inspection_registry, FormInspection, FormInspectionRegistry};
365pub use form_ir::{
366 lower_form_ir, FormInstanceIr, FormIrOperation, FormIrReport, FormRuntimeStorage,
367};
368pub use form_ir_optimization::{
369 optimize_form_ir, FormIrOptimizationMetrics, OptimizedFormIrReport,
370};
371pub use form_ownership::{
372 collect_form_ownership_graph, validate_form_ownership_graph, FormOwnershipEdge,
373 FormOwnershipEdgeKind, FormOwnershipGraph, FormOwnershipIntegrityDiagnostic,
374 FormOwnershipIntegrityKind, FormOwnershipNode, FormOwnershipNodeKey, FormOwnershipValidation,
375 FormReferenceEdge, FormReferenceKind,
376};
377pub use form_reset::{
378 collect_reset_products, FieldResetOperation, FieldResetStep, FormResetPlan, ResetProducts,
379};
380pub use form_serialization::{
381 collect_serialization_products, FormFieldSerializationConversion, FormSerializationFormat,
382 FormSerializationPlan, SerializationDeclarationFact, SerializationPlanStatus,
383 SerializationProducts, SerializedFieldPlan,
384};
385pub use form_submission::{
386 collect_submission_products, FormSubmissionPlan, SubmissionDeclarationCandidate,
387 SubmissionDeclarationViolation, SubmissionProducts, SubmitResetPolicy,
388};
389pub use form_submission_host::{
390 collect_submission_host_products, SubmissionHost, SubmissionHostCandidate,
391 SubmissionHostProducts, SubmissionHostViolation,
392};
393pub use form_tracking::{
394 collect_form_tracking_products, structurally_equal_serializable_values,
395 validate_dirty_tracking_graph, validate_touched_tracking_graph, DirtyTrackingGraph,
396 DirtyTrackingPlan, DirtyTransitionCause, DirtyTransitionPlan, FieldDirtyTracking,
397 FieldTouchedTracking, FormTrackingIntegrityDiagnostic, FormTrackingIntegrityKind,
398 FormTrackingProducts, FormTrackingValidation, TouchedTrackingGraph, TouchedTrackingPlan,
399 TouchedTransitionCause, TouchedTransitionPlan,
400};
401pub use form_validation::{
402 collect_validation_graph, collect_validation_products, validate_validation_graph,
403 ValidationCompatibility, ValidationDependencyCycle, ValidationDependencyDesignator,
404 ValidationGraph, ValidationGraphEdge, ValidationGraphEdgeKind,
405 ValidationGraphIntegrityDiagnostic, ValidationGraphIntegrityKind, ValidationGraphNode,
406 ValidationGraphNodeKey, ValidationGraphValidation, ValidationProducts, ValidationRule,
407 ValidationRuleArgument, ValidationRuleCandidate, ValidationRuleKind, ValidationRuleViolation,
408};
409pub use form_validation_plan::{
410 collect_validation_dependency_plans, validate_validation_dependency_plans,
411 BlockedFieldValidationDependency, FieldChangeSet, FieldChangeValidationSchedule,
412 FieldDependencyBlockReason, FieldValidationChangePlan, FieldValidationDependency,
413 FieldValidationSourceEntry, FieldValidationTargetEntry, FormValidationDependencyPlan,
414 ValidationDependencyPlanIntegrityDiagnostic, ValidationDependencyPlanIntegrityKind,
415 ValidationDependencyPlanValidation, ValidationDependencyPlans, ValidationPlanningStatus,
416};
417pub use html_codegen::generate_static_html;
418pub use instance_context::{
419 collect_instance_context_registry, ConsumerInstanceId, ConsumerInstanceRecord,
420 ContextDefaultSourceInstanceId, ContextSourceInstanceId, ContextSourceInstanceOwner,
421 InstanceContextRegistry, InstanceContextResolution, InstanceContextResolutionStatus,
422 InstanceContextValueSlotId, ProviderInstanceId, ProviderInstanceRecord,
423};
424pub use intermediate_representation::{
425 analyze_constant_propagation, analyze_dead_assignments, analyze_definition_uses,
426 analyze_liveness, analyze_reachability, analyze_reactive_cycles,
427 analyze_reactive_transitive_graph, analyze_use_definitions, build_reactive_graph,
428 compute_dominators, compute_post_dominators, computed_optimization_pipeline, inspect_dom_nodes,
429 lower_components_to_ir, optimize_computed_ir, optimize_context_ir, optimize_effect_ir,
430 plan_computed_evaluation, validate_context_ir, validate_effect_ir,
431 validate_intermediate_representation, validate_optimized_context_ir, ContextConsumerLoadId,
432 ContextIrReport, ContextSourceFunctionId, ContextValueSlotId, IntermediateRepresentation,
433 IrBinaryOperation, IrBlock, IrBlockId, IrBranchArm, IrBranchEdge, IrCfgCleanupPass,
434 IrCommonSubexpressionEliminationPass, IrComputedEvaluation, IrComputedEvaluationPlan,
435 IrConstant, IrConstantFoldingPass, IrConstantPropagationAnalysis, IrContextConsumerBinding,
436 IrContextLoad, IrContextSourceEvaluation, IrCopyPropagationPass, IrDeadAssignmentAnalysis,
437 IrDeadCodeEliminationPass, IrDefinitionUseAnalysis, IrDomAttribute, IrDomAttributeValue,
438 IrDomBinding, IrDomConditional, IrDomEvent, IrDomInspection, IrDomList, IrDomNode, IrDomNodeId,
439 IrDomNodeKind, IrDomText, IrDominatorTree, IrEffectCompletion, IrEffectExecution, IrFunction,
440 IrInstruction, IrInstructionId, IrInstructionKind, IrInstructionSimplificationPass,
441 IrLivenessAnalysis, IrLoop, IrLoopId, IrModule, IrOperand, IrOptimizationMetrics,
442 IrOptimizationPass, IrOptimizationPassReport, IrOptimizationPipeline, IrOptimizationReport,
443 IrPassManager, IrPostDominatorTree, IrReachabilityAnalysis, IrReactiveCycle,
444 IrReactiveCycleAnalysis, IrReactiveEdge, IrReactiveEdgeKind, IrReactiveGraph, IrReactiveNode,
445 IrReactiveNodeKind, IrReactiveTransitiveAnalysis, IrSchedulerInspection, IrStorage,
446 IrStorageId, IrTemplateEntrypoint, IrUnaryOperation, IrUpdateScheduler, IrUse, IrUseDefinition,
447 IrValidationDiagnostic, IrValue, IrValueDefinition, IrValueId, OptimizedContextIrReport,
448 OptimizedIrContextSourceEvaluation,
449};
450pub use layout_composition::{
451 build_layout_composition_plan_from_components_v1, build_layout_composition_plan_v1,
452 layout_composition_virtual_invocations_from_provenance_v1,
453 layout_composition_virtual_invocations_v1, LayoutCompositionEdgeV1, LayoutCompositionErrorV1,
454 LayoutCompositionPlanV1, LayoutCompositionRouteV1,
455};
456pub use metaframework_handoff::{
457 build_deployable_release_manifest_v1, build_static_request_handoff_v1,
458 deployable_release_manifest_json_v1, static_request_handoff_json_v1, DeployableArtifactV1,
459 DeployableReleaseManifestV1, StaticRequestHandoffV1, StaticRequestRouteV1,
460};
461pub use model::{
462 ClassSummary, DecoratorSummary, Diagnostic, RenderMethodSummary, Severity, SourceSummary, Span,
463};
464pub use module_graph::{
465 build_module_graph, ModuleEdge, ModuleEdgeKind, ModuleGraph, ModuleNode, ModuleTarget,
466};
467pub use opaque::{OpaqueActionResolution, OpaqueActionResolutionOutcome, OpaqueTerminalBinding};
468pub use ordinary_html_codegen::{
469 generate_ordinary_instance_html, generate_ordinary_instance_html_for_component,
470};
471pub use ordinary_template_instance::{
472 build_ordinary_template_instance_registry, validate_ordinary_template_instance_registry,
473 OrdinaryTemplateBindingKind, OrdinaryTemplateInstanceBindingRecord,
474 OrdinaryTemplateInstanceEventRecord, OrdinaryTemplateInstanceRegistry,
475 OrdinaryTemplateInstanceTargetRecord, OrdinaryTemplateTargetKind,
476 ORDINARY_TEMPLATE_INSTANCE_REGISTRY_VERSION,
477};
478pub use ordinary_template_integrity::{
479 ComputedInstanceSlotIntegrityCode, OrdinaryTemplateIntegrityCode,
480 StateInstanceStorageIntegrityCode,
481};
482pub use page_codegen::{
483 embed_opaque_runtime_artifact, generate_standalone_page,
484 generate_standalone_page_with_component_runtime,
485 generate_standalone_page_with_component_runtime_and_forms,
486 generate_standalone_page_with_computed_runtime, generate_standalone_page_with_context_runtime,
487 generate_standalone_page_with_effect_runtime, generate_standalone_page_with_resume_runtime,
488 generate_standalone_page_with_resume_runtime_and_resources,
489};
490pub use production_audit::{
491 ProductionRuntimeAuthority, PRODUCTION_RUNTIME_AUTHORITIES,
492 PRODUCTION_RUNTIME_REFINEMENT_INVARIANTS,
493};
494pub use production_bootstrap::{
495 build_production_bootstrap_plan, ProductionBootstrapBlock, ProductionBootstrapPlan,
496 ProductionRuntimeIndexes,
497};
498pub use production_chunk_graph::{
499 extract_production_chunk_graph, validate_production_chunk_graph, ProductionActivationChunkPlan,
500 ProductionChunkDependency, ProductionChunkExtractionError, ProductionChunkExtractionReport,
501 ProductionChunkGraph, ProductionChunkGraphValidationError, ProductionChunkKind,
502 ProductionChunkRecord, ProductionRootChunkInput, ProductionSharedChunkFailurePolicy,
503};
504pub use production_cleanup::{
505 build_production_destroy_plan, validate_production_cleanup_closure,
506 ProductionCleanupClosureViolation, ProductionCleanupKind, ProductionDestroyPlan,
507 ProductionOwnedRuntimeRecord,
508};
509pub use production_constant_pool::{
510 pool_production_constants, ConstantPoolConsumer, ConstantPoolingDecision,
511 ConstantPoolingReport, ProductionConstantCandidate, ProductionConstantPool,
512 ProductionConstantPoolEntry,
513};
514pub use production_deduplication::{
515 deduplicate_generated_programs, DeduplicatedProgramRegistry, ExecutableProgramCandidate,
516 ExecutableProgramCanonicalStream, ExecutableProgramFingerprintRegistry, ProgramAliasRecord,
517 ProgramDeduplicationReport,
518};
519pub use production_diagnostics::{
520 project_production_diagnostics, ProductionDiagnosticContract, ProductionDiagnosticFact,
521 ProductionDiagnosticKind, ProductionDiagnosticProvenance, ProductionProjectedDiagnostic,
522 PRODUCTION_DIAGNOSTIC_CATALOG,
523};
524pub use production_elimination::{
525 eliminate_unreachable_production_records, DeadProductEliminationReason,
526 DeadProductEliminationReport, OptimizationDecision, ProductionArtifactProjection,
527 ProductionExecutionRecord,
528};
529pub use production_module_emitter::{
530 emit_production_modules, validate_production_module, ProductionModuleLayout,
531 ProductionModuleRecord, ProductionModuleValidationError,
532};
533pub use production_optimization::{
534 BenchmarkFixtureId, ConstantPoolEntryId, ExecutableProgramFingerprint, OptimizationDecisionId,
535 OptimizationPolicyId, OptimizationReportId, PerformanceBudgetId, ProductionArtifactId,
536 ProductionChunkId, ProductionOptimizationDiagnosticReservation,
537 ProductionOptimizationIdentityParseError, ProductionOptimizationPolicy,
538 ProductionOptimizationPolicyV1, RuntimeTableId, SharedChunkCandidateId,
539 PRODUCTION_OPTIMIZATION_DIAGNOSTIC_RESERVATIONS,
540 PRODUCTION_OPTIMIZATION_INTEGRITY_RESERVATION_END,
541 PRODUCTION_OPTIMIZATION_INTEGRITY_RESERVATION_START,
542};
543pub use production_reachability::{
544 build_production_reachability_graph, ProductionExecutableRoot, ProductionProgramEdge,
545 ProductionReachabilityBlock, ProductionReachabilityGraph, ProductionReachabilityReason,
546 ProductionUnreachableRecord,
547};
548pub use production_reports::{
549 build_production_reports, optimization_report_json, runtime_cost_report_json,
550 OptimizationInspectionQuery, OptimizationReportV1, ProductionReportInputs,
551 RuntimeCostInspectionQuery, RuntimeCostReportV1, OPTIMIZATION_REPORT_SCHEMA_VERSION,
552 RUNTIME_COST_REPORT_SCHEMA_VERSION,
553};
554pub use production_runtime_artifact::{
555 build_production_runtime_artifact, build_production_runtime_table,
556 parse_production_runtime_artifact_v1, production_runtime_artifact_json,
557 validate_production_runtime_artifact, ProductionArtifactActivationEntry,
558 ProductionArtifactChunkRecord, ProductionArtifactEntry, ProductionArtifactIntegrity,
559 ProductionArtifactIntegrityViolation, ProductionOrdinalMapping, ProductionOrdinalWidth,
560 ProductionRuntimeArtifactV1, ProductionRuntimeTable, ProductionRuntimeTableRegistry,
561 PRODUCTION_RUNTIME_ARTIFACT_SCHEMA_VERSION, PRODUCTION_RUNTIME_TABLE_SCHEMA_VERSION,
562};
563pub use production_scheduler::{
564 build_production_patch_schedule, coalesce_production_binding_writes,
565 BindingWriteCoalescingDecision, BindingWriteCoalescingReport, ProductionPatchBatchKind,
566 ProductionPatchOperation, ProductionPatchSchedule,
567};
568pub use production_validation::{
569 validate_production_runtime_pipeline, ProductionRuntimeFailure, ProductionValidationEvidence,
570 ProductionValidationPhase, ProductionValidationResult,
571};
572pub use project_discovery::{
573 discover_project_v1, discover_semantic_packages_v1, DiscoveredProjectSourceV1,
574 DiscoveredProjectV1, ProjectDiscoveryErrorV1,
575};
576pub use provider::{collect_provider_entities, DuplicateProviderDeclaration, ProviderEntity};
577pub use resource::{
578 ResourceActivation, ResourceDeclaration, ResourceDeclarationError, ResourceEndpointBinding,
579 ResourceEndpointResolution, ResourceEndpointResolutionOutcome, ResourceInvalidationPolicy,
580 ResourceLifecycleError, ResourceLifecycleEvent, ResourceLifecycleState, ResourceRetryPolicy,
581};
582pub use resume_activation::{
583 build_resume_activation_plan, validate_resume_activation_plan, ResumeActivationBlock,
584 ResumeActivationBlockReason, ResumeActivationIntegrityCode,
585 ResumeActivationIntegrityDiagnostic, ResumeActivationPlan, ResumeActivationPolicy,
586 ResumeActivationPolicyDecision, ResumeActivationPrerequisite, RESUME_ACTIVATION_PLAN_VERSION,
587};
588pub use resume_anchor::{
589 build_resume_anchor_plan, validate_resume_anchor_plan, validate_resume_marker_html,
590 ResumeAnchorIntegrityCode, ResumeAnchorIntegrityDiagnostic, ResumeAnchorKind,
591 ResumeAnchorPlacement, ResumeAnchorPlan, ResumeAnchorPlanRecord, ResumeEventMarkerRecord,
592 RESUME_ANCHOR_PLAN_VERSION,
593};
594pub use resume_boundary::{
595 build_resume_boundary_graph, validate_resume_boundary_graph, ResumeBoundary,
596 ResumeBoundaryActivationIdentity, ResumeBoundaryActivationProgram,
597 ResumeBoundaryActivationReference, ResumeBoundaryBlock, ResumeBoundaryBlockSource,
598 ResumeBoundaryGraph, ResumeBoundaryIntegrityCode, ResumeBoundaryIntegrityDiagnostic,
599 ResumeBoundaryOwner, ResumeBoundaryOwnershipEdge, RESUME_BOUNDARY_GRAPH_VERSION,
600};
601pub use resume_capture::{
602 build_resume_capture_plan, capture_resume_snapshot, encode_resume_value,
603 resume_snapshot_artifact_json, resume_snapshot_json, validate_resume_capture_plan,
604 ResumeCaptureBlock, ResumeCaptureBlockReason, ResumeCaptureError, ResumeCaptureErrorKind,
605 ResumeCaptureInstruction, ResumeCaptureIntegrityCode, ResumeCaptureIntegrityDiagnostic,
606 ResumeCapturePlan, ResumeCaptureProgram, ResumeEncodedValue, ResumeEnvelopeWriterPlan,
607 ResumeSnapshotBoundaryV1, ResumeSnapshotV1, ResumeSnapshotValueRecordV1,
608 RuntimeQuiescenceState, RESUME_CAPTURE_MANIFEST_VERSION, RESUME_CAPTURE_PLAN_VERSION,
609 RESUME_SNAPSHOT_SCHEMA_VERSION,
610};
611pub use resume_chunk::{
612 build_resume_chunk_graph, validate_resume_chunk_graph, ResumeChunk, ResumeChunkBlock,
613 ResumeChunkBlockReason, ResumeChunkGraph, ResumeChunkIntegrityCode,
614 ResumeChunkIntegrityDiagnostic, ResumeChunkModulePlan, ResumeChunkProgram,
615 ResumeChunkProgramInclusion, ResumeChunkRootKind, RESUME_CHUNK_GRAPH_VERSION,
616};
617pub use resume_diagnostics::{
618 build_resume_diagnostic_products, project_resume_diagnostics,
619 project_resume_diagnostics_from_products, ResumeDiagnosticProducts,
620 ResumeDiagnosticReservation, ResumeProjectedDiagnostic, RESUME_DIAGNOSTIC_RESERVATIONS,
621 RESUME_INTEGRITY_RESERVATION_END, RESUME_INTEGRITY_RESERVATION_START,
622};
623pub use resume_identity::{
624 ComputedInstanceCacheSlotId, ComputedInstanceDirtySlotId, ResumeActivationId,
625 ResumeActivationRootKind, ResumeAnchorId, ResumeBoundaryId, ResumeBoundaryKind, ResumeBuildId,
626 ResumeCaptureProgramId, ResumeChunkGroupId, ResumeChunkId, ResumeEventId,
627 ResumeIdentityParseError, ResumeRestoreProgramId, ResumeSchemaId, ResumeSlotId,
628 ResumeSnapshotId, ResumeValueRecordId, StateInstanceSlotId, TemplateInstanceBindingId,
629 TemplateInstanceTargetId,
630};
631pub use resume_liveness::{
632 build_resume_liveness_plan, validate_resume_liveness_plan, ResumeExcludedSlot,
633 ResumeExistingSlot, ResumeLivenessBlock, ResumeLivenessBlockReason,
634 ResumeLivenessClassificationKind, ResumeLivenessClassificationRef, ResumeLivenessIntegrityCode,
635 ResumeLivenessIntegrityDiagnostic, ResumeLivenessOwner, ResumeLivenessPlan, ResumeLivenessSlot,
636 ResumeRecomputableSlot, ResumeRecomputationProof, ResumeRetainedSlot, ResumeRetentionReason,
637 RESUME_LIVENESS_PLAN_VERSION,
638};
639pub use resume_manifest::{
640 build_resume_manifest, compute_resume_build_id, parse_resume_manifest_v6, resume_manifest_json,
641 validate_resume_manifest, ResumeManifest, ResumeManifestActivationPolicy,
642 ResumeManifestActivationRecord, ResumeManifestAnchorRecord, ResumeManifestBoundaryKind,
643 ResumeManifestBoundaryRecord, ResumeManifestCaptureInstruction, ResumeManifestCaptureProgram,
644 ResumeManifestChunkRecord, ResumeManifestContextSlotRecord, ResumeManifestEffectRecord,
645 ResumeManifestEventRecord, ResumeManifestPhaseIComponentResumeRecord,
646 ResumeManifestRestoreInstruction, ResumeManifestRestoreInstructionRecord,
647 ResumeManifestRestoreProgram, ResumeManifestSlotSchemaRecord,
648 ResumeManifestValidationDiagnostic, RESUME_MANIFEST_SCHEMA_VERSION,
649 RESUME_RUNTIME_PROTOCOL_VERSION,
650};
651pub use resume_plan::{
652 build_resume_plan, ComponentInstanceResumePlan, FormFieldResumePlan, FormInstanceResumePlan,
653 ResumeComponentPlan, ResumeComputedPlan, ResumePlan, SlotBindingResumePlan,
654 StructuralRegionResumePlan,
655};
656pub use resume_restore::{
657 build_resume_restore_plan, validate_resume_restore_plan, ResumeRestoreApplicationSchedule,
658 ResumeRestoreBlock, ResumeRestoreBlockReason, ResumeRestoreInstruction,
659 ResumeRestoreInstructionRecord, ResumeRestoreIntegrityCode, ResumeRestoreIntegrityDiagnostic,
660 ResumeRestorePhase, ResumeRestorePlan, ResumeRestoreProgram, ResumeRestoreSchedulePhase,
661 ResumeRestoreSlotAssignment, RESUME_RESTORE_PLAN_VERSION,
662};
663pub use resume_schema::{
664 build_resume_schema_registry, resume_value_codec, validate_resume_schema_registry,
665 ResumeBoundarySchema, ResumeObjectPropertyCodec, ResumeSchemaBlock, ResumeSchemaBlockReason,
666 ResumeSchemaIntegrityCode, ResumeSchemaIntegrityDiagnostic, ResumeSchemaRegistry,
667 ResumeSlotSchema, ResumeValueCodec, RESUME_SCHEMA_REGISTRY_VERSION,
668};
669pub use route_graph::{
670 build_file_route_graph_from_components_v1, build_file_route_graph_v1, build_route_graph,
671 build_static_route_publication_v1, build_validated_file_route_graph_from_components_v1,
672 build_validated_file_route_graph_v1, build_validated_route_graph_v1, route_manifest_json_v1,
673 route_manifest_v1, FileRouteGraphV1, FileRouteNodeV1, RouteGraph, RouteGraphError,
674 RouteManifestEntryV1, RouteManifestV1, RouteNode,
675};
676pub use route_loader::{
677 build_route_loader_plan_v1, route_loader_plan_json_v1, RouteLoaderBindingV1,
678 RouteLoaderPlanErrorV1, RouteLoaderPlanV1, RouteLoaderRouteV1,
679 ROUTE_LOADER_PLAN_SCHEMA_VERSION,
680};
681pub use route_server_action::{
682 build_route_server_action_plan_v1, route_server_action_plan_json_v1,
683 RouteServerActionBindingV1, RouteServerActionPlanErrorV1, RouteServerActionPlanV1,
684 RouteServerActionRouteV1, ROUTE_SERVER_ACTION_PLAN_SCHEMA_VERSION,
685};
686pub use runtime_codegen::generate_runtime_stub;
687pub use runtime_component::{
688 build_runtime_component_registry, RuntimeComponentContextBindingRecord,
689 RuntimeComponentDefinitionRecord, RuntimeComponentInitializationBatch,
690 RuntimeComponentInstanceRecord, RuntimeComponentRegistry, RuntimeComponentSlotBindingRecord,
691 RUNTIME_COMPONENT_REGISTRY_SCHEMA_CONTRACT_VERSION,
692};
693pub use runtime_component_artifact::{
694 build_runtime_component_artifact, runtime_component_artifact_json,
695 validate_runtime_component_artifact, RuntimeComponentArtifact,
696 RUNTIME_COMPONENT_ARTIFACT_SCHEMA_VERSION,
697};
698pub use runtime_computed::{
699 build_runtime_computed_registry, ComputedCacheSlotId, ComputedDirtyFlagId,
700 RuntimeComputedCacheSlot, RuntimeComputedDirtyFlag, RuntimeComputedRecord,
701 RuntimeComputedRegistry,
702};
703pub use runtime_computed_artifact::{
704 build_runtime_computed_artifact, runtime_computed_artifact_json, RuntimeComputedArtifact,
705 RuntimeComputedArtifactDirtyFlag, RuntimeComputedArtifactEvaluation,
706 RuntimeComputedArtifactInstruction, RuntimeComputedArtifactInvalidation,
707 RuntimeComputedArtifactResourceInvalidation, RuntimeComputedArtifactSerialization,
708 RUNTIME_COMPUTED_ARTIFACT_SCHEMA_VERSION,
709};
710pub use runtime_context::{
711 build_runtime_context_registry, validate_runtime_context_registry,
712 RuntimeContextConsumerRecord, RuntimeContextEvaluationBatch, RuntimeContextRegistry,
713 RuntimeContextRegistryValidationDiagnostic, RuntimeContextSourceKind,
714 RuntimeContextSourceRecord, RUNTIME_CONTEXT_REGISTRY_SCHEMA_CONTRACT_VERSION,
715};
716pub use runtime_context_artifact::{
717 build_runtime_context_artifact, runtime_context_artifact_json, RuntimeContextArtifact,
718 SerializedContextActionUpdatePlan, SerializedContextBatchId, SerializedContextConsumerBinding,
719 SerializedContextEvaluationBatch, SerializedContextExecutionBoundary,
720 SerializedContextInstruction, SerializedContextInstructionKind, SerializedContextProgram,
721 SerializedContextSource, SerializedContextSourceKind, RUNTIME_CONTEXT_ARTIFACT_SCHEMA_VERSION,
722};
723pub use runtime_effect::{
724 build_runtime_effect_registry, RuntimeActionBatchEffectTrigger, RuntimeEffectRecord,
725 RuntimeEffectRegistry, RuntimeInitialEffectTrigger,
726};
727pub use runtime_effect_artifact::{
728 build_runtime_effect_artifact, runtime_effect_artifact_json, RuntimeEffectArtifact,
729 RuntimeEffectArtifactActionTrigger, RuntimeEffectArtifactCapabilityInstructionKind,
730 RuntimeEffectArtifactCapabilityOperation, RuntimeEffectArtifactEffect,
731 RuntimeEffectArtifactExecutionBoundary, RuntimeEffectArtifactExecutionPolicy,
732 RuntimeEffectArtifactInitialTrigger, RuntimeEffectArtifactInstruction,
733 RuntimeEffectArtifactPrerequisiteBatch, RuntimeEffectArtifactProgram,
734 RuntimeEffectArtifactRenderBoundary, RUNTIME_EFFECT_ARTIFACT_SCHEMA_VERSION,
735};
736pub use runtime_form_artifact::{
737 build_runtime_forms_artifact, runtime_forms_artifact_json, validate_runtime_forms_artifact,
738 RuntimeFormsArtifact, RuntimeFormsArtifactBinding, RuntimeFormsArtifactDependency,
739 RuntimeFormsArtifactField, RuntimeFormsArtifactFieldProgram, RuntimeFormsArtifactFieldSlots,
740 RuntimeFormsArtifactForm, RuntimeFormsArtifactInstance, RuntimeFormsArtifactPrograms,
741 RuntimeFormsArtifactReset, RuntimeFormsArtifactRule, RuntimeFormsArtifactSerialization,
742 RuntimeFormsArtifactSubmission, RuntimeFormsArtifactValidation,
743 RUNTIME_FORM_ARTIFACT_SCHEMA_VERSION,
744};
745pub use runtime_form_registry::{
746 build_runtime_form_registry, RuntimeFormInstanceRecord, RuntimeFormRecord, RuntimeFormRegistry,
747 RUNTIME_FORM_REGISTRY_VERSION,
748};
749pub use runtime_opaque_artifact::{
750 build_runtime_opaque_artifact, build_runtime_opaque_artifact_with_modules,
751 runtime_opaque_artifact_json, validate_runtime_opaque_artifact, RuntimeOpaqueArtifact,
752 RuntimeOpaqueArtifactActivation, RuntimeOpaqueArtifactBuildError,
753 RuntimeOpaqueArtifactValidationError, RUNTIME_OPAQUE_ARTIFACT_SCHEMA_VERSION,
754};
755pub use runtime_resource_artifact::{
756 build_runtime_resource_artifact, build_runtime_resource_artifact_with_modules,
757 runtime_resource_artifact_json, validate_runtime_resource_artifact, RuntimeResourceArtifact,
758 RuntimeResourceArtifactActivation, RuntimeResourceArtifactBuildError,
759 RuntimeResourceArtifactDeclaration, RuntimeResourceArtifactEndpoint,
760 RuntimeResourceArtifactValidationError, RUNTIME_RESOURCE_ARTIFACT_SCHEMA_VERSION,
761};
762pub use semantic_capability::{
763 build_semantic_capability_registry, semantic_capability_matrix_text,
764 semantic_capability_migration_text, semantic_capability_registry_json, SemanticCapability,
765 SemanticCapabilityClass, SemanticCapabilityRegistry, SemanticCapabilityStatus,
766 SEMANTIC_CAPABILITY_REGISTRY_SCHEMA_VERSION,
767};
768pub use semantic_graph::{
769 build_semantic_graph, semantic_graph_json, SemanticGraph, SemanticGraphConsumer,
770 SemanticGraphContext, SemanticGraphEdge, SemanticGraphEdgeKind, SemanticGraphNode,
771 SemanticGraphNodeKind, SemanticGraphProvenance, SemanticGraphProvider,
772 SEMANTIC_GRAPH_SCHEMA_VERSION,
773};
774pub use semantic_id::{
775 ComponentInstanceId, ComponentInvocationId, ComponentRootId, ComponentStructuralRegionId,
776 ConsumerId, ContextDeclarationCandidateId, ContextId, DirtyTrackingPlanId, EffectId,
777 EffectStatementId, FieldBindingId, FieldDependencyId, FieldId, FieldResetOperationId,
778 FieldTrackingId, FormDeclarationCandidateId, FormFieldBindingCandidateId,
779 FormFieldDeclarationCandidateId, FormFieldDirtySlotId, FormFieldTouchedSlotId,
780 FormFieldValidationSlotId, FormFieldValueSlotId, FormId, FormInstanceId, FormOwnershipGraphId,
781 FormSubmissionStateSlotId, FormValidationAggregateSlotId, ProviderId, ResetPlanId,
782 ResourceActivationId, ResourceId, SemanticId, SemanticOwner, SerializationPlanId,
783 SlotBindingId, SlotContentFragmentId, SlotDeclarationCandidateId, SlotId, SlotOutletId,
784 SubmissionDeclarationCandidateId, SubmissionHostCandidateId, SubmissionHostId,
785 SubmissionPlanId, TemplatePositionId, TouchedTrackingPlanId, ValidationDependencyCycleId,
786 ValidationGraphId, ValidationPlanId, ValidationRuleCandidateId, ValidationRuleId,
787};
788pub use semantic_package::{
789 parse_semantic_package_contract, SemanticPackageContract, SemanticPackageContractError,
790 SemanticPackageExport, SemanticPackageKind, SemanticPackageOpaqueExecutionBoundary,
791 SemanticPackageOpaqueResumePolicy, SemanticPackageOpaqueTerminal, SemanticPackagePureOperation,
792 SemanticPackageResolutionTable, SemanticPackageResourceCancellation,
793 SemanticPackageResourceEndpoint, SemanticPackageResourceExecutionBoundary,
794 SemanticPackageResourceResumePolicy, SemanticPackageRouteLoader,
795 SemanticPackageRouteLoaderFailure, SemanticPackageRouteLoaderInput,
796 SemanticPackageServerAction, SemanticPackageServerActionInput,
797 SemanticPackageServerActionResponse, SemanticPackageServerCachePolicy,
798 SemanticPackageServerCacheScope, SEMANTIC_PACKAGE_CONTRACT_SCHEMA_VERSION,
799};
800pub use semantic_package_runtime::{
801 SemanticPackageRuntimeModuleError, SemanticPackageRuntimeModuleKey,
802 SemanticPackageRuntimeModuleTable,
803};
804pub use semantic_provenance::SourceProvenance;
805pub use semantic_reference::{SemanticReference, SemanticReferenceKind};
806pub use semantic_type::{
807 boundary_compatibility, dom_binding_contract, infer_serializable_value_type, is_assignable,
808 is_state_initializer_assignable, operator_result_type, semantic_type_text,
809 serialization_compatibility, state_initializer_value_type, BoundaryCompatibility,
810 BuiltinTypeAuthority, ComputedValueType, DomBindingContract, DomBindingKind,
811 EffectCompatibility, EffectOperationClassification, EffectStatementTypeRecord,
812 ExecutionBoundary, ObjectType, ResolvedDeclaredSemanticType, ResourceExecutionBoundary,
813 ResourceType, SemanticOperator, SemanticType, SemanticTypeAlias, SemanticTypeAssignment,
814 SemanticTypeId, SemanticTypeModel, SemanticTypeStatus, SerializationCompatibility,
815 TypeDiagnosticCode, TypeDiagnosticFamily,
816};
817pub use shared_chunk_candidate::{
818 plan_shared_lazy_chunk_candidates, SharedChunkCandidate, SharedChunkCandidatePlan,
819 SharedChunkConsumerRoot, SharedChunkProgramOccurrence, SharedChunkRejectionReason,
820 SharedChunkSavingsCalculation,
821};
822pub use slot::{collect_slot_entities, SlotEntity};
823pub use slot_binding::{
824 collect_slot_bindings, collect_slot_bindings_with_virtual_invocations, SlotBinding,
825 SlotBindingRegistry, SlotBindingStatus,
826};
827pub use slot_content::{
828 collect_slot_composition, SlotCompositionRegistry, SlotContentFragment,
829 SlotContentFragmentStatus, SlotContentFragmentViolation, SlotOutlet, SlotOutletStatus,
830 SlotOutletViolation,
831};
832pub use state_instance_storage::{
833 build_state_instance_storage_registry, validate_state_instance_storage_registry,
834 StateInstanceStorageRecord, StateInstanceStorageRegistry,
835 STATE_INSTANCE_STORAGE_REGISTRY_VERSION,
836};
837pub use summarize::summarize_source;
838pub use symbol_table::{
839 build_symbol_table, ModuleSymbol, ModuleSymbolTable, SymbolDiagnostic, SymbolKind, SymbolTable,
840};
841pub use template_graph::{
842 build_template_graph, AttributeValue, ConditionalNode, ElementNode, FragmentNode, ListNode,
843 TemplateAttribute, TemplateChild, TemplateGraph, TemplateNode, TemplateNodeId,
844};
845pub use template_manifest::{
846 build_template_manifest, build_template_manifest_from_asm, template_manifest_json,
847 validate_template_manifest, ManifestAction, ManifestBindingTarget, ManifestComponent,
848 ManifestEvent, ManifestEventKind, ManifestFormBinding, ManifestFormHost, ManifestNode,
849 ManifestOperation, ManifestOrdinaryBinding, ManifestOrdinaryEvent, ManifestOrdinaryTarget,
850 ManifestTemplate, TemplateManifest, TEMPLATE_MANIFEST_SCHEMA_VERSION,
851};
852pub use template_semantics::{
853 build_template_semantic_entities, TemplateSemanticEntity, TemplateSemanticKind,
854 TemplateSemanticScope,
855};
856
857#[cfg(test)]
858mod tests {
859 use super::*;
860 use std::{collections::BTreeMap, path::Path};
861
862 #[test]
863 fn summarizes_component_decorator_class_and_render_method() {
864 let source = r#"
865@component("x-counter")
866class Counter extends Component {
867 render() {
868 return <button>Count</button>;
869 }
870}
871"#;
872
873 let summary = summarize_source("Counter.tsx", source);
874
875 assert_eq!(summary.component_decorators.len(), 1);
876 assert_eq!(
877 summary.component_decorators[0].argument.as_deref(),
878 Some("x-counter")
879 );
880 assert_eq!(summary.class_declarations.len(), 1);
881 assert_eq!(summary.class_declarations[0].name, "Counter");
882 assert_eq!(summary.render_methods.len(), 1);
883 assert!(summary.has_tsx_like_syntax);
884 }
885
886 #[test]
887 fn emits_diagnostics_for_empty_source() {
888 let summary = summarize_source("Empty.tsx", "");
889 assert!(summary
890 .diagnostics
891 .iter()
892 .any(|diagnostic| diagnostic.code == "PS0001"));
893 }
894
895 #[test]
896 fn fixture_0001_source_summary_explain_text_matches_expected() {
897 let fixture_root = Path::new(env!("CARGO_MANIFEST_DIR"))
898 .join("../..")
899 .join("fixtures/0001-source-summary");
900
901 let input_path = fixture_root.join("input/Counter.tsx");
902 let expected_path = fixture_root.join("expected/explain.txt");
903
904 let source = std::fs::read_to_string(&input_path).expect("failed to read fixture input");
905 let expected = std::fs::read_to_string(&expected_path)
906 .expect("failed to read expected explain output");
907
908 let summary = summarize_source("fixtures/0001-source-summary/input/Counter.tsx", &source);
909
910 let actual = explain_text(&summary);
911
912 assert_eq!(actual, expected);
913 }
914
915 #[test]
916 fn fixture_0001_source_summary_explain_json_matches_expected() {
917 let fixture_root = Path::new(env!("CARGO_MANIFEST_DIR"))
918 .join("../..")
919 .join("fixtures/0001-source-summary");
920
921 let input_path = fixture_root.join("input/Counter.tsx");
922 let expected_path = fixture_root.join("expected/explain.json");
923
924 let source = std::fs::read_to_string(&input_path).expect("failed to read fixture input");
925 let expected = std::fs::read_to_string(&expected_path)
926 .expect("failed to read expected JSON explain output");
927
928 let summary = summarize_source("fixtures/0001-source-summary/input/Counter.tsx", &source);
929
930 let actual = explain_json(&summary);
931
932 let actual_json: serde_json::Value =
933 serde_json::from_str(&actual).expect("actual explain JSON is invalid");
934 let expected_json: serde_json::Value =
935 serde_json::from_str(&expected).expect("expected explain JSON fixture is invalid");
936
937 assert_eq!(actual_json, expected_json);
938 }
939
940 #[test]
941 #[allow(clippy::too_many_lines)]
942 fn builds_component_graph_from_parsed_counter() {
943 let source = include_str!("../../../fixtures/0001-source-summary/input/Counter.tsx");
944
945 let parsed =
946 presolve_parser::parse_file("fixtures/0001-source-summary/input/Counter.tsx", source);
947
948 let graph = build_component_graph(&parsed);
949
950 assert!(graph.diagnostics.is_empty());
951
952 let component = graph.components.first().expect("expected component");
953
954 assert_eq!(component.class_name, "Counter");
955 assert_eq!(component.id.as_str(), "component:x-counter");
956 assert_eq!(component.owner, SemanticOwner::Application);
957 assert_eq!(component.element_name.as_deref(), Some("x-counter"));
958 assert_eq!(component.route_path.as_deref(), Some("/counter"));
959
960 assert_eq!(component.state_fields.len(), 1);
961 assert_eq!(component.state_fields[0].name, "count");
962 assert_eq!(
963 component.state_fields[0].id.as_str(),
964 "component:x-counter/state:count"
965 );
966 assert_eq!(
967 component.state_fields[0].owner,
968 SemanticOwner::entity(component.id.clone())
969 );
970 assert_eq!(
971 component.state_fields[0].initial_value,
972 Some(SerializableValue::Number("0".to_string()))
973 );
974
975 let method_names = component
976 .methods
977 .iter()
978 .map(|method| method.name.as_str())
979 .collect::<Vec<_>>();
980
981 assert_eq!(method_names, vec!["increment", "render"]);
982 assert_eq!(
983 component.methods[0].id.as_str(),
984 "component:x-counter/method:increment"
985 );
986 assert_eq!(
987 component.methods[0].owner,
988 SemanticOwner::entity(component.id.clone())
989 );
990 assert_eq!(
991 component.actions,
992 vec![ComponentAction {
993 id: SemanticId::component(Some("x-counter"), "Counter").action("increment", 0),
994 owner: SemanticOwner::entity(
995 SemanticId::component(Some("x-counter"), "Counter").method("increment"),
996 ),
997 method: "increment".to_string(),
998 operation: StateOperation::AddAssign(SerializableValue::Number("1".to_string())),
999 field: "count".to_string(),
1000 }]
1001 );
1002
1003 let render = component.render.as_ref().expect("expected render model");
1004
1005 assert_eq!(render.root_element.as_deref(), Some("button"));
1006 assert_eq!(render.attributes.len(), 1);
1007 assert_eq!(render.attributes[0].name, "onClick");
1008 assert!(matches!(
1009 render.attributes[0].value,
1010 RenderAttributeValue::Expression(_)
1011 ));
1012 assert_eq!(render.bindings, vec!["this.count"]);
1013 assert_eq!(render.root_span.expect("expected root span").line, 12);
1014 assert_eq!(render.root_span.expect("expected root span").column, 7);
1015 assert_eq!(render.event_handlers.len(), 1);
1016 assert_eq!(
1017 render.event_handlers[0].id.as_str(),
1018 "component:x-counter/event:click:0"
1019 );
1020 assert_eq!(
1021 render.event_handlers[0].owner,
1022 SemanticOwner::entity(component.id.template())
1023 );
1024 assert_eq!(render.event_handlers[0].event, "click");
1025 assert_eq!(render.event_handlers[0].handler, "this.increment");
1026 assert_eq!(render.event_handlers[0].span.line, 12);
1027 assert_eq!(render.event_handlers[0].span.column, 15);
1028 assert_eq!(render.children.len(), 2);
1029
1030 let RenderChild::Text { value, span } = &render.children[0] else {
1031 panic!("expected text child");
1032 };
1033 assert_eq!(value, "Count:");
1034 assert_eq!(span.line, 13);
1035 assert_eq!(span.column, 9);
1036
1037 let RenderChild::Binding { expression, span } = &render.children[1] else {
1038 panic!("expected binding child");
1039 };
1040 assert_eq!(expression, "this.count");
1041 assert_eq!(span.line, 13);
1042 assert_eq!(span.column, 16);
1043
1044 assert_eq!(graph.references.len(), 2);
1045 assert_eq!(graph.references[0].kind, SemanticReferenceKind::ActionState);
1046 assert_eq!(
1047 graph.references[0].source,
1048 SemanticId::component(Some("x-counter"), "Counter").action("increment", 0)
1049 );
1050 assert_eq!(
1051 graph.references[0].target,
1052 SemanticId::component(Some("x-counter"), "Counter").state_field("count")
1053 );
1054 assert_eq!(
1055 graph.references[0].provenance.path,
1056 Path::new("fixtures/0001-source-summary/input/Counter.tsx")
1057 );
1058 assert_eq!(graph.references[0].provenance.span.line, 7);
1059
1060 assert_eq!(graph.references[1].kind, SemanticReferenceKind::EventMethod);
1061 assert_eq!(
1062 graph.references[1].source,
1063 SemanticId::component(Some("x-counter"), "Counter").event_handler("click", 0)
1064 );
1065 assert_eq!(
1066 graph.references[1].target,
1067 SemanticId::component(Some("x-counter"), "Counter").method("increment")
1068 );
1069 assert_eq!(
1070 graph.references[1].provenance.path,
1071 Path::new("fixtures/0001-source-summary/input/Counter.tsx")
1072 );
1073 assert_eq!(graph.references[1].provenance.span.line, 12);
1074
1075 assert_eq!(graph.provenance[&component.id].span.line, 1);
1076 assert_eq!(graph.provenance[&component.state_fields[0].id].span.line, 4);
1077 assert_eq!(graph.provenance[&component.methods[0].id].span.line, 6);
1078 assert_eq!(graph.provenance[&component.actions[0].id].span.line, 7);
1079 assert_eq!(graph.provenance[&component.id.template()].span.line, 10);
1080 assert_eq!(graph.provenance[&render.event_handlers[0].id].span.line, 12);
1081 }
1082
1083 #[test]
1084 fn lowers_method_parameters_into_canonical_method_metadata() {
1085 let parsed = presolve_parser::parse_file(
1086 "src/Parameters.tsx",
1087 r#"
1088@component("x-parameters")
1089class Parameters extends Component {
1090 save(title: string, retries?: number) {}
1091}
1092"#,
1093 );
1094
1095 let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1096 let method = &graph.components[0].methods[0];
1097
1098 assert_eq!(method.name, "save");
1099 assert_eq!(
1100 method
1101 .parameters
1102 .iter()
1103 .map(|parameter| parameter.name.as_str())
1104 .collect::<Vec<_>>(),
1105 vec!["title", "retries"]
1106 );
1107 assert_eq!(method.parameters[0].span.line, 4);
1108 assert_eq!(method.parameters[1].span.line, 4);
1109 }
1110
1111 #[test]
1112 fn assembles_application_semantic_model_from_existing_graphs() {
1113 let source = include_str!("../../../fixtures/0001-source-summary/input/Counter.tsx");
1114 let parsed =
1115 presolve_parser::parse_file("fixtures/0001-source-summary/input/Counter.tsx", source);
1116
1117 let asm = build_application_semantic_model(&parsed);
1118 let component = &asm.components[0];
1119
1120 assert!(asm.diagnostics.is_empty());
1121 assert_eq!(asm.templates.len(), 1);
1122 assert!(asm.template_entities.len() >= 3);
1123 assert_eq!(asm.references.len(), 4);
1124 assert_eq!(asm.ownership.len(), asm.provenance.len());
1125 assert_eq!(asm.ownership[&component.id], SemanticOwner::Application);
1126 assert_eq!(
1127 asm.ownership[&component.state_fields[0].id],
1128 SemanticOwner::entity(component.id.clone())
1129 );
1130 assert_eq!(
1131 asm.ownership[&component.actions[0].id],
1132 SemanticOwner::entity(component.methods[0].id.clone())
1133 );
1134 assert_eq!(
1135 asm.ownership[&asm.templates[0].id],
1136 SemanticOwner::entity(component.id.clone())
1137 );
1138 assert_eq!(asm.provenance[&asm.templates[0].id].span.line, 10);
1139
1140 assert!(matches!(
1141 asm.entity(&component.id),
1142 Some(SemanticEntity::Component(_))
1143 ));
1144 assert!(matches!(
1145 asm.entity(&component.state_fields[0].id),
1146 Some(SemanticEntity::StateField(_))
1147 ));
1148 assert_eq!(asm.component(&component.id), Some(component));
1149 assert_eq!(asm.template(&asm.templates[0].id), Some(&asm.templates[0]));
1150 assert_eq!(
1151 asm.owner(&component.actions[0].id),
1152 Some(&component.actions[0].owner)
1153 );
1154 assert_eq!(asm.provenance(&asm.templates[0].id).unwrap().span.line, 10);
1155 let template_binding = asm
1156 .template_entities
1157 .iter()
1158 .find(|entity| entity.kind == TemplateSemanticKind::Binding)
1159 .expect("template binding entity");
1160 assert!(matches!(
1161 asm.entity(&template_binding.id),
1162 Some(SemanticEntity::TemplateEntity(_))
1163 ));
1164 assert_eq!(
1165 asm.template_entities_for(&asm.templates[0].id).len(),
1166 asm.template_entities.len()
1167 );
1168 assert_eq!(asm.references_from(&component.actions[0].id).len(), 1);
1169 assert_eq!(asm.references_to(&component.state_fields[0].id).len(), 2);
1170 assert_eq!(asm.references_to(&component.methods[0].id).len(), 2);
1171 assert!(validate_application_semantic_model(&asm).is_empty());
1172
1173 let mut invalid = asm.clone();
1174 invalid.provenance.remove(&component.actions[0].id);
1175 let diagnostics = validate_application_semantic_model(&invalid);
1176 let codes = diagnostics
1177 .iter()
1178 .map(|diagnostic| diagnostic.code.as_str())
1179 .collect::<Vec<_>>();
1180 assert!(codes.contains(&"PSASM1002"));
1181 assert!(codes.contains(&"PSASM1006"));
1182
1183 let mut invalid_type = asm.clone();
1184 let state_id = component.state_fields[0].id.clone();
1185 invalid_type
1186 .semantic_types
1187 .assignments
1188 .get_mut(&state_id)
1189 .expect("state type")
1190 .id = SemanticTypeId::for_subject(&component.id);
1191 let diagnostics = validate_application_semantic_model(&invalid_type);
1192 assert!(diagnostics
1193 .iter()
1194 .any(|diagnostic| diagnostic.code == "PSASM1102"));
1195
1196 let dependencies = DependencyAnalysisPass.analyze(&asm);
1197 assert_eq!(dependencies.dependencies[&component.actions[0].id].len(), 1);
1198 assert_eq!(
1199 dependencies.dependents[&component.state_fields[0].id].len(),
1200 2
1201 );
1202 }
1203
1204 #[test]
1205 fn carries_declared_state_types_into_component_and_asm_data() {
1206 let parsed = presolve_parser::parse_file(
1207 "src/Panel.tsx",
1208 r#"
1209@component("x-panel")
1210class Panel extends Component {
1211 count: number = state(0);
1212}
1213"#,
1214 );
1215
1216 let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1217 let graph_type = graph.components[0].state_fields[0]
1218 .declared_type
1219 .as_ref()
1220 .expect("declared state type");
1221
1222 assert_eq!(graph_type.text, "number");
1223 assert_eq!(graph_type.provenance.path, Path::new("src/Panel.tsx"));
1224 assert_eq!(graph_type.provenance.span.line, 4);
1225 assert_eq!(graph_type.provenance.span.column, 8);
1226 assert_eq!(graph_type.kind, Some(DeclaredStateTypeKind::Number));
1227
1228 let asm = build_application_semantic_model(&parsed);
1229 assert_eq!(
1230 asm.components[0].state_fields[0].declared_type,
1231 Some(graph_type.clone())
1232 );
1233 }
1234
1235 #[test]
1236 fn lowers_and_evaluates_constant_arithmetic_state_initializers() {
1237 let parsed = presolve_parser::parse_file(
1238 "src/ArithmeticState.tsx",
1239 r#"
1240@component("x-arithmetic-state")
1241class ArithmeticState extends Component {
1242 total: number = state((1 + 2) * 3);
1243 difference: number = state(10 - 3);
1244 quotient: number = state(10 / 2);
1245 remainder: number = state(10 % 3);
1246
1247 render() {
1248 return <output>{this.total}</output>;
1249 }
1250}
1251"#,
1252 );
1253 let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1254 let field = &graph.components[0].state_fields[0];
1255
1256 assert_eq!(
1257 field
1258 .initial_expression
1259 .as_ref()
1260 .map(ToString::to_string)
1261 .as_deref(),
1262 Some("((1 + 2) * 3)")
1263 );
1264 assert_eq!(
1265 field.initial_value,
1266 Some(SerializableValue::Number("9".to_string()))
1267 );
1268 assert_eq!(
1269 graph.components[0].state_fields[1].initial_value,
1270 Some(SerializableValue::Number("7".to_string()))
1271 );
1272 assert_eq!(
1273 graph.components[0].state_fields[2].initial_value,
1274 Some(SerializableValue::Number("5".to_string()))
1275 );
1276 assert_eq!(
1277 graph.components[0].state_fields[3].initial_value,
1278 Some(SerializableValue::Number("1".to_string()))
1279 );
1280 assert!(graph.diagnostics.is_empty());
1281 }
1282
1283 #[test]
1284 fn reports_invalid_constant_arithmetic_state_initializers() {
1285 let parsed = presolve_parser::parse_file(
1286 "src/ArithmeticState.tsx",
1287 r#"
1288@component("x-arithmetic-state")
1289class ArithmeticState extends Component {
1290 total: number = state(10 / 0);
1291}
1292"#,
1293 );
1294 let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1295 let diagnostic = graph
1296 .diagnostics
1297 .iter()
1298 .find(|diagnostic| diagnostic.code == "PSC1022")
1299 .expect("arithmetic diagnostic");
1300
1301 assert!(diagnostic.message.contains("division or remainder by zero"));
1302 assert_eq!(
1303 diagnostic
1304 .provenance
1305 .as_ref()
1306 .map(|provenance| provenance.span.line),
1307 Some(4)
1308 );
1309 }
1310
1311 #[test]
1312 fn lowers_and_evaluates_constant_comparison_state_initializers() {
1313 let parsed = presolve_parser::parse_file(
1314 "src/ComparisonState.tsx",
1315 r#"
1316@component("x-comparison-state")
1317class ComparisonState extends Component {
1318 equal: boolean = state(3 === 3);
1319 notEqual: boolean = state(3 !== 4);
1320 lessThan: boolean = state(2 < 3);
1321 lessThanOrEqual: boolean = state(3 <= 3);
1322 greaterThan: boolean = state(4 > 3);
1323 ready: boolean = state(((1 + 2) * 3) >= 9);
1324
1325 render() {
1326 return <output>{this.ready}</output>;
1327 }
1328}
1329"#,
1330 );
1331 let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1332 let fields = &graph.components[0].state_fields;
1333
1334 assert_eq!(
1335 fields[0]
1336 .initial_expression
1337 .as_ref()
1338 .map(ToString::to_string)
1339 .as_deref(),
1340 Some("(3 === 3)")
1341 );
1342 for field in fields {
1343 assert_eq!(field.initial_value, Some(SerializableValue::Boolean(true)));
1344 }
1345 assert_eq!(
1346 fields[5]
1347 .initial_expression
1348 .as_ref()
1349 .map(ToString::to_string)
1350 .as_deref(),
1351 Some("(((1 + 2) * 3) >= 9)")
1352 );
1353 assert!(graph.diagnostics.is_empty());
1354 }
1355
1356 #[test]
1357 fn reports_invalid_constant_comparison_state_initializers() {
1358 let parsed = presolve_parser::parse_file(
1359 "src/ComparisonState.tsx",
1360 r#"
1361@component("x-comparison-state")
1362class ComparisonState extends Component {
1363 ready: boolean = state((10 / 0) >= 1);
1364}
1365"#,
1366 );
1367 let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1368 let diagnostic = graph
1369 .diagnostics
1370 .iter()
1371 .find(|diagnostic| diagnostic.code == "PSC1023")
1372 .expect("comparison diagnostic");
1373
1374 assert!(diagnostic.message.contains("division or remainder by zero"));
1375 assert_eq!(
1376 diagnostic
1377 .provenance
1378 .as_ref()
1379 .map(|provenance| provenance.span.line),
1380 Some(4)
1381 );
1382 }
1383
1384 #[test]
1385 fn lowers_and_evaluates_constant_logical_state_initializers() {
1386 let parsed = presolve_parser::parse_file(
1387 "src/LogicalState.tsx",
1388 r#"
1389@component("x-logical-state")
1390class LogicalState extends Component {
1391 both: boolean = state((1 < 2) && (3 >= 3));
1392 either: boolean = state(false || (10 !== 4));
1393 shortAnd: boolean = state(false && ((10 / 0) > 1));
1394 shortOr: boolean = state(true || ((10 / 0) > 1));
1395
1396 render() {
1397 return <output>{this.both}</output>;
1398 }
1399}
1400"#,
1401 );
1402 let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1403 let fields = &graph.components[0].state_fields;
1404
1405 assert_eq!(
1406 fields[0]
1407 .initial_expression
1408 .as_ref()
1409 .map(ToString::to_string)
1410 .as_deref(),
1411 Some("((1 < 2) && (3 >= 3))")
1412 );
1413 assert_eq!(
1414 fields
1415 .iter()
1416 .map(|field| field.initial_value.clone())
1417 .collect::<Vec<_>>(),
1418 vec![
1419 Some(SerializableValue::Boolean(true)),
1420 Some(SerializableValue::Boolean(true)),
1421 Some(SerializableValue::Boolean(false)),
1422 Some(SerializableValue::Boolean(true)),
1423 ]
1424 );
1425 assert!(graph.diagnostics.is_empty());
1426 }
1427
1428 #[test]
1429 fn reports_evaluated_invalid_constant_logical_state_initializers() {
1430 let parsed = presolve_parser::parse_file(
1431 "src/LogicalState.tsx",
1432 r#"
1433@component("x-logical-state")
1434class LogicalState extends Component {
1435 ready: boolean = state(true && ((10 / 0) > 1));
1436}
1437"#,
1438 );
1439 let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1440 let diagnostic = graph
1441 .diagnostics
1442 .iter()
1443 .find(|diagnostic| diagnostic.code == "PSC1024")
1444 .expect("logical diagnostic");
1445
1446 assert!(diagnostic.message.contains("division or remainder by zero"));
1447 assert_eq!(
1448 diagnostic
1449 .provenance
1450 .as_ref()
1451 .map(|provenance| provenance.span.line),
1452 Some(4)
1453 );
1454 }
1455
1456 #[test]
1457 fn lowers_and_evaluates_constant_nullish_state_initializers() {
1458 let parsed = presolve_parser::parse_file(
1459 "src/NullishState.tsx",
1460 r#"
1461@component("x-nullish-state")
1462class NullishState extends Component {
1463 label: string = state(null ?? "fallback");
1464 total: number = state(5 ?? (10 / 0));
1465
1466 render() {
1467 return <output>{this.label}</output>;
1468 }
1469}
1470"#,
1471 );
1472 let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1473 let fields = &graph.components[0].state_fields;
1474
1475 assert_eq!(
1476 fields[0]
1477 .initial_expression
1478 .as_ref()
1479 .map(ToString::to_string)
1480 .as_deref(),
1481 Some("(null ?? \"fallback\")")
1482 );
1483 assert_eq!(
1484 fields[0].initial_value,
1485 Some(SerializableValue::String("fallback".to_string()))
1486 );
1487 assert_eq!(
1488 fields[1].initial_value,
1489 Some(SerializableValue::Number("5".to_string()))
1490 );
1491 assert!(graph.diagnostics.is_empty());
1492 }
1493
1494 #[test]
1495 fn lowers_and_evaluates_constant_unary_state_initializers() {
1496 let parsed = presolve_parser::parse_file(
1497 "src/UnaryState.tsx",
1498 r#"
1499@component("x-unary-state")
1500class UnaryState extends Component {
1501 negated: boolean = state(!(1 < 2));
1502 signed: number = state(-(1 + 2));
1503 render() { return <output>{this.signed}</output>; }
1504}
1505"#,
1506 );
1507 let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1508 assert_eq!(
1509 graph.components[0].state_fields[0].initial_value,
1510 Some(SerializableValue::Boolean(false))
1511 );
1512 assert_eq!(
1513 graph.components[0].state_fields[1].initial_value,
1514 Some(SerializableValue::Number("-3".to_string()))
1515 );
1516 }
1517
1518 #[test]
1519 fn reports_reached_invalid_constant_nullish_state_initializers() {
1520 let parsed = presolve_parser::parse_file(
1521 "src/NullishState.tsx",
1522 r#"
1523@component("x-nullish-state")
1524class NullishState extends Component {
1525 total: number = state(null ?? (10 / 0));
1526}
1527"#,
1528 );
1529 let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1530 let diagnostic = graph
1531 .diagnostics
1532 .iter()
1533 .find(|diagnostic| diagnostic.code == "PSC1025")
1534 .expect("nullish diagnostic");
1535 assert!(diagnostic.message.contains("division or remainder by zero"));
1536 }
1537
1538 #[test]
1539 fn classifies_exact_primitive_declared_state_types() {
1540 let source =
1541 include_str!("../../../fixtures/0025-typed-state-annotations/input/TypedState.tsx");
1542 let parsed = presolve_parser::parse_file(
1543 "fixtures/0025-typed-state-annotations/input/TypedState.tsx",
1544 source,
1545 );
1546 let graph = build_component_graph_for_module(&parsed);
1547 let kinds = graph.components[0]
1548 .state_fields
1549 .iter()
1550 .map(|field| {
1551 (
1552 field.name.as_str(),
1553 field
1554 .declared_type
1555 .as_ref()
1556 .and_then(|declared_type| declared_type.kind),
1557 )
1558 })
1559 .collect::<Vec<_>>();
1560
1561 assert_eq!(
1562 kinds,
1563 vec![
1564 ("count", Some(DeclaredStateTypeKind::Number)),
1565 ("status", None),
1566 ("title", Some(DeclaredStateTypeKind::String)),
1567 ("enabled", Some(DeclaredStateTypeKind::Boolean)),
1568 ("empty", Some(DeclaredStateTypeKind::Null)),
1569 ]
1570 );
1571 }
1572
1573 #[test]
1574 fn reports_primitive_declared_state_initializer_mismatches() {
1575 let source = include_str!(
1576 "../../../fixtures/0027-declared-state-type-diagnostics/input/InvalidTypedState.tsx"
1577 );
1578 let parsed = presolve_parser::parse_file(
1579 "fixtures/0027-declared-state-type-diagnostics/input/InvalidTypedState.tsx",
1580 source,
1581 );
1582 let graph = build_component_graph_for_module(&parsed);
1583 let folded = ConstantFoldingPass.transform(
1584 &build_application_semantic_model_from_component_graph(&graph),
1585 );
1586 let diagnostics = folded
1587 .diagnostics
1588 .iter()
1589 .map(|diagnostic| (diagnostic.code.as_str(), diagnostic.message.as_str()))
1590 .collect::<Vec<_>>();
1591
1592 assert_eq!(diagnostics.len(), 6);
1593 assert!(diagnostics.iter().all(|(code, _)| *code == "PSC1016"));
1594
1595 let provenance = folded.diagnostics[0]
1596 .provenance
1597 .as_ref()
1598 .expect("mismatch diagnostic provenance");
1599 assert_eq!(
1600 provenance.path,
1601 Path::new("fixtures/0027-declared-state-type-diagnostics/input/InvalidTypedState.tsx")
1602 );
1603 assert_eq!(provenance.span.line, 3);
1604 assert_eq!(provenance.span.column, 8);
1605 }
1606
1607 #[test]
1608 fn reports_primitive_declared_state_action_assignment_mismatches() {
1609 let source = include_str!(
1610 "../../../fixtures/0028-primitive-action-type-diagnostics/input/InvalidTypedActions.tsx"
1611 );
1612 let parsed = presolve_parser::parse_file(
1613 "fixtures/0028-primitive-action-type-diagnostics/input/InvalidTypedActions.tsx",
1614 source,
1615 );
1616 let graph = build_component_graph_for_module(&parsed);
1617 let folded = ConstantFoldingPass.transform(
1618 &build_application_semantic_model_from_component_graph(&graph),
1619 );
1620 let diagnostics = folded
1621 .diagnostics
1622 .iter()
1623 .filter(|diagnostic| diagnostic.code == "PSC1017")
1624 .map(|diagnostic| (diagnostic.code.as_str(), diagnostic.message.as_str()))
1625 .collect::<Vec<_>>();
1626
1627 assert_eq!(diagnostics.len(), 6);
1628 assert!(diagnostics.iter().all(|(code, _)| *code == "PSC1017"));
1629 assert!(diagnostics.iter().any(|(_, message)| {
1630 message.contains("state field `status`") && message.contains("assigns `number`")
1631 }));
1632 assert!(diagnostics.iter().any(|(_, message)| {
1633 message.contains("state field `collection`") && message.contains("assigns `tuple`")
1634 }));
1635
1636 let provenance = folded
1637 .diagnostics
1638 .iter()
1639 .find(|diagnostic| diagnostic.code == "PSC1017")
1640 .expect("action mismatch diagnostic")
1641 .provenance
1642 .as_ref()
1643 .expect("action mismatch diagnostic provenance");
1644 assert_eq!(
1645 provenance.path,
1646 Path::new(
1647 "fixtures/0028-primitive-action-type-diagnostics/input/InvalidTypedActions.tsx"
1648 )
1649 );
1650 assert_eq!(provenance.span.line, 11);
1651 assert_eq!(provenance.span.column, 5);
1652 }
1653
1654 #[test]
1655 fn reports_non_boolean_primitive_toggle_actions() {
1656 let source = include_str!(
1657 "../../../fixtures/0029-primitive-toggle-type-diagnostics/input/InvalidTypedToggles.tsx"
1658 );
1659 let parsed = presolve_parser::parse_file(
1660 "fixtures/0029-primitive-toggle-type-diagnostics/input/InvalidTypedToggles.tsx",
1661 source,
1662 );
1663 let graph = build_component_graph_for_module(&parsed);
1664 let folded = ConstantFoldingPass.transform(
1665 &build_application_semantic_model_from_component_graph(&graph),
1666 );
1667 let diagnostics = folded
1668 .diagnostics
1669 .iter()
1670 .map(|diagnostic| (diagnostic.code.as_str(), diagnostic.message.as_str()))
1671 .collect::<Vec<_>>();
1672
1673 assert_eq!(diagnostics.len(), 4);
1674 assert!(diagnostics.iter().all(|(code, _)| *code == "PSC1018"));
1675
1676 let provenance = folded.diagnostics[0]
1677 .provenance
1678 .as_ref()
1679 .expect("toggle diagnostic provenance");
1680 assert_eq!(
1681 provenance.path,
1682 Path::new(
1683 "fixtures/0029-primitive-toggle-type-diagnostics/input/InvalidTypedToggles.tsx"
1684 )
1685 );
1686 assert_eq!(provenance.span.line, 10);
1687 assert_eq!(provenance.span.column, 5);
1688 }
1689
1690 #[test]
1691 fn reports_non_numeric_primitive_increment_and_decrement_actions() {
1692 let source = include_str!(
1693 "../../../fixtures/0030-primitive-numeric-action-type-diagnostics/input/InvalidTypedNumericActions.tsx"
1694 );
1695 let parsed = presolve_parser::parse_file(
1696 "fixtures/0030-primitive-numeric-action-type-diagnostics/input/InvalidTypedNumericActions.tsx",
1697 source,
1698 );
1699 let graph = build_component_graph_for_module(&parsed);
1700 let folded = ConstantFoldingPass.transform(
1701 &build_application_semantic_model_from_component_graph(&graph),
1702 );
1703 let diagnostics = folded
1704 .diagnostics
1705 .iter()
1706 .map(|diagnostic| (diagnostic.code.as_str(), diagnostic.message.as_str()))
1707 .collect::<Vec<_>>();
1708
1709 assert_eq!(diagnostics.len(), 4);
1710 assert!(diagnostics.iter().all(|(code, _)| *code == "PSC1019"));
1711
1712 let provenance = folded.diagnostics[0]
1713 .provenance
1714 .as_ref()
1715 .expect("numeric action diagnostic provenance");
1716 assert_eq!(
1717 provenance.path,
1718 Path::new(
1719 "fixtures/0030-primitive-numeric-action-type-diagnostics/input/InvalidTypedNumericActions.tsx"
1720 )
1721 );
1722 assert_eq!(provenance.span.line, 10);
1723 assert_eq!(provenance.span.column, 5);
1724 }
1725
1726 #[test]
1727 fn reports_compound_numeric_action_target_and_operand_mismatches() {
1728 let source = include_str!(
1729 "../../../fixtures/0031-primitive-compound-action-type-diagnostics/input/InvalidTypedCompoundActions.tsx"
1730 );
1731 let parsed = presolve_parser::parse_file(
1732 "fixtures/0031-primitive-compound-action-type-diagnostics/input/InvalidTypedCompoundActions.tsx",
1733 source,
1734 );
1735 let graph = build_component_graph_for_module(&parsed);
1736 let folded = ConstantFoldingPass.transform(
1737 &build_application_semantic_model_from_component_graph(&graph),
1738 );
1739 let diagnostics = folded
1740 .diagnostics
1741 .iter()
1742 .map(|diagnostic| (diagnostic.code.as_str(), diagnostic.message.as_str()))
1743 .collect::<Vec<_>>();
1744
1745 assert_eq!(diagnostics.len(), 7);
1746 assert_eq!(
1747 diagnostics
1748 .iter()
1749 .filter(|(code, _)| *code == "PSC1020")
1750 .count(),
1751 3
1752 );
1753 assert_eq!(
1754 diagnostics
1755 .iter()
1756 .filter(|(code, _)| *code == "PSC1021")
1757 .count(),
1758 4
1759 );
1760
1761 let provenance = folded.diagnostics[0]
1762 .provenance
1763 .as_ref()
1764 .expect("compound action diagnostic provenance");
1765 assert_eq!(
1766 provenance.path,
1767 Path::new(
1768 "fixtures/0031-primitive-compound-action-type-diagnostics/input/InvalidTypedCompoundActions.tsx"
1769 )
1770 );
1771 assert_eq!(provenance.span.line, 9);
1772 assert_eq!(provenance.span.column, 5);
1773 }
1774
1775 #[test]
1776 fn assembles_application_semantic_model_from_multiple_files() {
1777 let unit = CompilationUnit::parse_sources([
1778 (
1779 "src/Zeta.tsx",
1780 r#"
1781@component("x-zeta")
1782class Zeta extends Component {
1783 render() {
1784 return <div>Zeta</div>;
1785 }
1786}
1787"#,
1788 ),
1789 (
1790 "src/Alpha.tsx",
1791 r#"
1792@component("x-alpha")
1793class Alpha extends Component {
1794 render() {
1795 return <div>Alpha</div>;
1796 }
1797}
1798"#,
1799 ),
1800 ]);
1801
1802 let asm = build_application_semantic_model_for_unit(&unit);
1803
1804 assert_eq!(
1805 unit.files()
1806 .iter()
1807 .map(|file| file.path.to_string_lossy().into_owned())
1808 .collect::<Vec<_>>(),
1809 vec!["src/Alpha.tsx", "src/Zeta.tsx"]
1810 );
1811 assert_eq!(
1812 asm.components
1813 .iter()
1814 .map(|component| component.id.as_str())
1815 .collect::<Vec<_>>(),
1816 vec![
1817 "module:src/Alpha.tsx/component:x-alpha",
1818 "module:src/Zeta.tsx/component:x-zeta"
1819 ]
1820 );
1821 assert!(asm.diagnostics.is_empty());
1822 assert!(validate_application_semantic_model(&asm).is_empty());
1823 }
1824
1825 #[test]
1826 fn component_graph_reports_semantic_errors() {
1827 let source =
1828 include_str!("../../../fixtures/0003-semantic-errors/input/BrokenSemantics.tsx");
1829
1830 let parsed = presolve_parser::parse_file(
1831 "fixtures/0003-semantic-errors/input/BrokenSemantics.tsx",
1832 source,
1833 );
1834
1835 let graph = build_component_graph(&parsed);
1836
1837 let codes = graph
1838 .diagnostics
1839 .iter()
1840 .map(|diagnostic| diagnostic.code.as_str())
1841 .collect::<Vec<_>>();
1842
1843 assert!(codes.contains(&"PSC1001"));
1844 assert!(codes.contains(&"PSC1003"));
1845 assert!(codes.contains(&"PSC1004"));
1846 assert!(graph.references.is_empty());
1847 }
1848
1849 #[test]
1850 fn component_graph_reports_unsupported_event_errors() {
1851 let source = r#"
1852@component("x-counter")
1853class Counter extends Component {
1854 count = state(0);
1855
1856 increment() {
1857 this.count++;
1858 }
1859
1860 render() {
1861 return <button onMouseover={() => this.increment()}>Count: {this.count}</button>;
1862 }
1863}
1864"#;
1865
1866 let parsed = presolve_parser::parse_file("UnsupportedEvent.tsx", source);
1867
1868 let graph = build_component_graph(&parsed);
1869
1870 assert!(graph
1871 .diagnostics
1872 .iter()
1873 .any(|diagnostic| diagnostic.code == "PSC1005"));
1874 }
1875
1876 #[test]
1877 fn component_graph_validates_static_action_parameter_bindings() {
1878 let source = r#"
1879@component("x-parameterized")
1880class Parameterized extends Component {
1881 label = state("Ready");
1882
1883 @action() setLabel(value: string) {
1884 this.label = value;
1885 }
1886
1887 render() {
1888 return <button onClick={() => this.setLabel(1)}>{this.label}</button>;
1889 }
1890}
1891"#;
1892
1893 let parsed = presolve_parser::parse_file("Parameterized.tsx", source);
1894 let graph = build_component_graph(&parsed);
1895 let codes = graph
1896 .diagnostics
1897 .iter()
1898 .map(|diagnostic| diagnostic.code.as_str())
1899 .collect::<Vec<_>>();
1900
1901 assert!(codes.contains(&"PSC1043"));
1902 assert!(!codes.contains(&"PSC1041"));
1903 assert!(!codes.contains(&"PSC1042"));
1904 }
1905
1906 #[test]
1907 fn component_graph_rejects_unbound_action_parameters() {
1908 let source = r#"
1909@component("x-parameterized")
1910class Parameterized extends Component {
1911 label = state("Ready");
1912
1913 @action() setLabel(value: string) {
1914 this.label = value;
1915 }
1916
1917 render() {
1918 return <button onClick={this.setLabel}>{this.label}</button>;
1919 }
1920}
1921"#;
1922
1923 let parsed = presolve_parser::parse_file("UnboundParameterized.tsx", source);
1924 let graph = build_component_graph(&parsed);
1925
1926 assert!(graph
1927 .diagnostics
1928 .iter()
1929 .any(|diagnostic| diagnostic.code == "PSC1042"));
1930 }
1931
1932 #[test]
1933 fn component_graph_requires_action_decorator_for_parameter_state_assignment() {
1934 let source = r#"
1935@component("x-parameterized")
1936class Parameterized extends Component {
1937 label = state("Ready");
1938
1939 setLabel(value: string) {
1940 this.label = value;
1941 }
1942
1943 render() {
1944 return <button onClick={() => this.setLabel("Locked")}>{this.label}</button>;
1945 }
1946}
1947"#;
1948
1949 let parsed = presolve_parser::parse_file("UndecoratedParameterized.tsx", source);
1950 let graph = build_component_graph(&parsed);
1951
1952 assert!(graph.diagnostics.iter().any(|diagnostic| {
1953 diagnostic.code == "PSC1041" && diagnostic.message.contains("requires @action()")
1954 }));
1955 }
1956
1957 #[test]
1958 fn component_graph_rejects_action_parameter_state_type_mismatch() {
1959 let source = r#"
1960@component("x-parameterized")
1961class Parameterized extends Component {
1962 count = state(0);
1963
1964 @action() setCount(value: string) {
1965 this.count = value;
1966 }
1967
1968 render() {
1969 return <button onClick={() => this.setCount("Locked")}>{this.count}</button>;
1970 }
1971}
1972"#;
1973
1974 let parsed = presolve_parser::parse_file("MismatchedParameterized.tsx", source);
1975 let graph = build_component_graph(&parsed);
1976
1977 assert!(graph
1978 .diagnostics
1979 .iter()
1980 .any(|diagnostic| diagnostic.code == "PSC1044"));
1981 }
1982
1983 #[test]
1984 fn component_graph_lowers_serializable_action_local_to_state_assignment() {
1985 let source = r#"
1986@component("x-action-local")
1987class ActionLocal extends Component {
1988 label = state("Ready");
1989
1990 @action() lock() {
1991 const next = "Locked";
1992 this.label = next;
1993 }
1994
1995 render() {
1996 return <button onClick={this.lock}>{this.label}</button>;
1997 }
1998}
1999"#;
2000
2001 let parsed = presolve_parser::parse_file("ActionLocal.tsx", source);
2002 let graph = build_component_graph(&parsed);
2003
2004 assert!(graph.diagnostics.is_empty(), "{:?}", graph.diagnostics);
2005 assert!(matches!(
2006 graph.components[0].actions[0].operation,
2007 StateOperation::Assign(SerializableValue::String(ref value)) if value == "Locked"
2008 ));
2009 }
2010
2011 #[test]
2012 fn component_graph_lowers_structured_serializable_action_local_to_state_assignment() {
2013 let source = r#"
2014@component("x-action-local-record")
2015class ActionLocalRecord extends Component {
2016 profile = state({ name: "Ready", roles: ["reader"] });
2017
2018 @action() promote() {
2019 const next = { name: "Locked", roles: ["writer", "admin"] };
2020 this.profile = next;
2021 }
2022
2023 render() {
2024 return <button onClick={this.promote}>Promote</button>;
2025 }
2026}
2027"#;
2028
2029 let parsed = presolve_parser::parse_file("ActionLocalRecord.tsx", source);
2030 let graph = build_component_graph(&parsed);
2031
2032 assert!(graph.diagnostics.is_empty(), "{:?}", graph.diagnostics);
2033 assert!(matches!(
2034 graph.components[0].actions[0].operation,
2035 StateOperation::Assign(SerializableValue::Object(_))
2036 ));
2037 }
2038
2039 #[test]
2040 fn component_graph_retains_resource_declaration_facts_before_package_resolution() {
2041 let parsed = presolve_parser::parse_file(
2042 "ResourceFact.tsx",
2043 r#"
2044@component("x-resource-fact")
2045class ResourceFact extends Component {
2046 @resource("profile") profile!: string;
2047 render() { return <div>Profile</div>; }
2048}
2049"#,
2050 );
2051 let graph = build_component_graph(&parsed);
2052 let facts = &graph.components[0].resource_declaration_candidates;
2053 assert_eq!(facts.len(), 1);
2054 assert_eq!(facts[0].field, "profile");
2055 assert!(facts[0].decorator_invoked);
2056 assert_eq!(facts[0].decorator_argument_count, 1);
2057 assert_eq!(facts[0].endpoint_designator.as_deref(), Some("profile"));
2058 assert!(graph.diagnostics.is_empty(), "{:?}", graph.diagnostics);
2059 }
2060
2061 #[test]
2062 fn component_graph_retains_and_validates_opaque_action_declarations() {
2063 let parsed = presolve_parser::parse_file(
2064 "OpaqueActions.tsx",
2065 r#"
2066@component("x-opaque-actions")
2067class OpaqueActions extends Component {
2068 @action() @opaque("@acme/analytics", "trackPurchase")
2069 track(): void {}
2070
2071 @action() @opaque("@acme/analytics")
2072 missingExport(): void {}
2073
2074 @opaque("@acme/analytics", "trackPurchase")
2075 missingAction(): void {}
2076
2077 @action() @opaque("@acme/analytics", "trackPurchase")
2078 writesState(): void { this.count++; }
2079
2080 count = state(0);
2081
2082 render() { return <button onClick={this.track}>Buy</button>; }
2083}
2084"#,
2085 );
2086
2087 let graph = build_component_graph(&parsed);
2088 let component = &graph.components[0];
2089 assert_eq!(component.opaque_action_facts.len(), 4);
2090
2091 let track = component
2092 .opaque_action_facts
2093 .iter()
2094 .find(|fact| fact.method_name == "track")
2095 .expect("retained track declaration");
2096 assert_eq!(
2097 track.id.as_str(),
2098 "component:x-opaque-actions/opaque-activation:track"
2099 );
2100 assert_eq!(track.package.as_deref(), Some("@acme/analytics"));
2101 assert_eq!(track.export.as_deref(), Some("trackPurchase"));
2102 assert!(track.is_action);
2103 assert!(track.action_invoked);
2104 assert!(!track.has_body_effects);
2105
2106 assert_eq!(
2107 graph
2108 .diagnostics
2109 .iter()
2110 .filter(|diagnostic| diagnostic.code == "PSC1130")
2111 .count(),
2112 3,
2113 "{:?}",
2114 graph.diagnostics
2115 );
2116 }
2117
2118 #[test]
2119 fn resolves_opaque_action_through_an_integrity_checked_terminal_package_contract() {
2120 let unit = CompilationUnit::parse_sources([(
2121 "src/Checkout.tsx",
2122 r#"
2123import { trackPurchase } from "@acme/analytics";
2124
2125@component("x-checkout")
2126class Checkout extends Component {
2127 @action() @opaque("@acme/analytics", "trackPurchase")
2128 track(): void {}
2129
2130 render() { return <button onClick={this.track}>Buy</button>; }
2131}
2132"#,
2133 )]);
2134 let contract = parse_semantic_package_contract(
2135 r#"{"schema_version":1,"package":"@acme/analytics","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"trackPurchase":{"kind":"opaque","type_signature":"() -> void","runtime_module":"dist/track.js","resume_policy":"cold_fallback","opaque_terminal":{"execution_boundary":"client","resume":"cold_fallback"}}}}"#,
2136 )
2137 .expect("opaque terminal contract");
2138 let mut packages = SemanticPackageResolutionTable::default();
2139 packages
2140 .insert("@acme/analytics".into(), contract)
2141 .expect("unique contract");
2142
2143 let model = build_application_semantic_model_for_unit_with_packages(&unit, &packages);
2144 assert_eq!(model.opaque_action_resolutions.len(), 1);
2145 let resolution = &model.opaque_action_resolutions[0];
2146 assert_eq!(
2147 resolution.activation.as_str(),
2148 "module:src/Checkout.tsx/component:x-checkout/opaque-activation:track"
2149 );
2150 let OpaqueActionResolutionOutcome::Resolved(binding) = &resolution.outcome else {
2151 panic!(
2152 "expected resolved opaque terminal: {:?}",
2153 resolution.outcome
2154 );
2155 };
2156 assert_eq!(binding.package, "@acme/analytics");
2157 assert_eq!(binding.version, "1.2.3");
2158 assert_eq!(binding.export, "trackPurchase");
2159 assert_eq!(binding.runtime_module, "dist/track.js");
2160 assert!(model
2161 .diagnostics
2162 .iter()
2163 .all(|diagnostic| diagnostic.code != "PSC1130" && diagnostic.code != "PSC1131"));
2164 }
2165
2166 #[test]
2167 fn opaque_action_rejects_a_nonopaque_package_export() {
2168 let unit = CompilationUnit::parse_sources([(
2169 "src/Checkout.tsx",
2170 r#"
2171import { format } from "date-kit";
2172
2173@component("x-checkout")
2174class Checkout extends Component {
2175 @action() @opaque("date-kit", "format")
2176 track(): void {}
2177
2178 render() { return <button onClick={this.track}>Buy</button>; }
2179}
2180"#,
2181 )]);
2182 let contract = parse_semantic_package_contract(
2183 r#"{"schema_version":1,"package":"date-kit","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"format":{"kind":"pure","type_signature":"(Date) -> string","runtime_module":"dist/format.js","resume_policy":"input_only"}}}"#,
2184 )
2185 .expect("pure contract");
2186 let mut packages = SemanticPackageResolutionTable::default();
2187 packages.insert("date-kit".into(), contract).unwrap();
2188
2189 let model = build_application_semantic_model_for_unit_with_packages(&unit, &packages);
2190 assert!(matches!(
2191 model.opaque_action_resolutions[0].outcome,
2192 OpaqueActionResolutionOutcome::NonOpaqueBinding {
2193 kind: SemanticPackageKind::Pure,
2194 ..
2195 }
2196 ));
2197 assert!(model
2198 .diagnostics
2199 .iter()
2200 .any(|diagnostic| diagnostic.code == "PSC1131"));
2201 }
2202
2203 #[test]
2204 fn resolves_resource_source_designator_through_integrity_checked_package_contract() {
2205 let unit = CompilationUnit::parse_sources([(
2206 "src/Profile.tsx",
2207 r#"
2208import { loadProfile } from "profile-service";
2209
2210@component("x-profile")
2211class Profile extends Component {
2212 @resource("loadProfile") profile!: Resource<string, string>;
2213 render() { return <div>Profile</div>; }
2214}
2215"#,
2216 )]);
2217 let contract = parse_semantic_package_contract(
2218 r#"{"schema_version":1,"package":"profile-service","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"loadProfile":{"kind":"resource","type_signature":"(ProfileKey) -> Resource<Profile, ProfileError>","runtime_module":"dist/load-profile.js","resume_policy":"snapshot","resource_endpoint":{"execution_boundary":"shared","cancellation":"abort","resume":"snapshot"}}}}"#,
2219 )
2220 .expect("resource contract");
2221 let mut packages = SemanticPackageResolutionTable::default();
2222 packages
2223 .insert("profile-service".into(), contract)
2224 .expect("unique package contract");
2225
2226 let model = build_application_semantic_model_for_unit_with_packages(&unit, &packages);
2227 assert_eq!(model.resource_endpoint_resolutions.len(), 1);
2228 let ResourceEndpointResolutionOutcome::Resolved(endpoint) =
2229 &model.resource_endpoint_resolutions[0].outcome
2230 else {
2231 panic!("expected resolved resource endpoint");
2232 };
2233 assert_eq!(endpoint.local_name, "loadProfile");
2234 assert_eq!(endpoint.package, "profile-service");
2235 assert_eq!(endpoint.version, "1.2.3");
2236 assert_eq!(endpoint.export, "loadProfile");
2237 assert_eq!(
2238 endpoint.endpoint.execution_boundary,
2239 SemanticPackageResourceExecutionBoundary::Shared
2240 );
2241 assert_eq!(model.resource_declarations.len(), 1);
2242 let declaration = model
2243 .resource_declarations
2244 .values()
2245 .next()
2246 .expect("resource declaration");
2247 assert_eq!(declaration.name, "profile");
2248 assert_eq!(declaration.data_type, SemanticType::String);
2249 assert_eq!(declaration.error_type, SemanticType::String);
2250 assert_eq!(
2251 declaration.execution_boundary,
2252 ResourceExecutionBoundary::Shared
2253 );
2254 assert_eq!(model.resource_activations.len(), 1);
2255 assert!(model.resource_activations.values().all(|activation| {
2256 activation.declaration == declaration.id
2257 && activation.state == ResourceLifecycleState::Idle
2258 }));
2259 assert!(
2260 model
2261 .diagnostics
2262 .iter()
2263 .all(|diagnostic| { diagnostic.code != "PSC1046" && diagnostic.code != "PSC1128" }),
2264 "{:?}",
2265 model.diagnostics
2266 );
2267 assert!(matches!(
2268 model
2269 .semantic_types
2270 .assignments
2271 .get(declaration.id.as_semantic_id())
2272 .map(|assignment| &assignment.semantic_type),
2273 Some(SemanticType::Resource(resource))
2274 if resource.data.as_ref() == &SemanticType::String
2275 && resource.error.as_ref() == &SemanticType::String
2276 ));
2277 }
2278
2279 #[test]
2280 fn lowers_direct_computed_resource_data_projection_with_exact_declaration_identity() {
2281 let unit = CompilationUnit::parse_sources([(
2282 "src/Profile.tsx",
2283 r#"
2284import { loadProfile } from "profile-service";
2285
2286@component("x-profile")
2287class Profile extends Component {
2288 @resource("loadProfile") profile!: Resource<string, string>;
2289 @computed() get profileName(): string | null { return this.profile.data; }
2290 render() { return <div>{this.profileName}</div>; }
2291}
2292"#,
2293 )]);
2294 let contract = parse_semantic_package_contract(
2295 r#"{"schema_version":1,"package":"profile-service","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"loadProfile":{"kind":"resource","type_signature":"(ProfileKey) -> Resource<Profile, ProfileError>","runtime_module":"dist/load-profile.js","resume_policy":"snapshot","resource_endpoint":{"execution_boundary":"shared","cancellation":"abort","resume":"snapshot"}}}}"#,
2296 )
2297 .expect("resource contract");
2298 let mut packages = SemanticPackageResolutionTable::default();
2299 packages
2300 .insert("profile-service".into(), contract)
2301 .expect("unique package contract");
2302
2303 let model = build_application_semantic_model_for_unit_with_packages(&unit, &packages);
2304 assert!(model.diagnostics.is_empty(), "{:?}", model.diagnostics);
2305 let declaration = model
2306 .resource_declarations
2307 .values()
2308 .next()
2309 .expect("resource declaration");
2310 let computed = model.components[0].id.computed("profileName");
2311 assert!(model.references.iter().any(|reference| {
2312 reference.kind == SemanticReferenceKind::ComputedResource
2313 && reference.source == computed
2314 && reference.target == *declaration.id.as_semantic_id()
2315 }));
2316 assert_eq!(
2317 model.semantic_types.computed_values[&computed].semantic_type,
2318 SemanticType::Union(vec![SemanticType::Null, SemanticType::String])
2319 );
2320
2321 let ir = lower_components_to_ir(&model);
2322 assert!(ir
2323 .modules
2324 .iter()
2325 .flat_map(|module| &module.functions)
2326 .any(|function| {
2327 function
2328 .blocks
2329 .iter()
2330 .flat_map(|block| &block.instructions)
2331 .any(|instruction| {
2332 matches!(
2333 &instruction.kind,
2334 IrInstructionKind::LoadResource { declaration: loaded }
2335 if loaded == declaration.id.as_semantic_id()
2336 )
2337 })
2338 }));
2339 let artifact = build_runtime_computed_artifact(&model, &ir);
2340 assert!(artifact.resource_invalidations.iter().any(|invalidation| {
2341 invalidation.declaration == declaration.id.as_str()
2342 && invalidation.dependents == vec![computed.to_string()]
2343 }));
2344 assert!(artifact.evaluations.iter().any(|evaluation| {
2345 evaluation.computed == computed.as_str()
2346 && evaluation.program.instructions.iter().any(|instruction| {
2347 matches!(
2348 instruction,
2349 RuntimeComputedArtifactInstruction::LoadResource { declaration: loaded, .. }
2350 if loaded == declaration.id.as_str()
2351 )
2352 })
2353 }));
2354
2355 let chained_unit = CompilationUnit::parse_sources([(
2356 "src/ChainedProfile.tsx",
2357 r#"
2358import { loadProfile } from "profile-service";
2359
2360@component("x-chained-profile")
2361class ChainedProfile extends Component {
2362 @resource("loadProfile") profile!: Resource<string, string>;
2363 @computed() get invalid() { return this.profile.data.length; }
2364 render() { return <div>Profile</div>; }
2365}
2366"#,
2367 )]);
2368 let chained =
2369 build_application_semantic_model_for_unit_with_packages(&chained_unit, &packages);
2370 assert!(chained.diagnostics.iter().any(|diagnostic| {
2371 diagnostic.code == ComputedDiagnosticCode::UnsupportedBody.as_str()
2372 && diagnostic.message.contains("may only directly project")
2373 }));
2374 }
2375
2376 #[test]
2377 fn component_graph_reports_duplicate_event_errors() {
2378 let parsed = presolve_parser::ParsedFile {
2379 path: "DuplicateEvent.tsx".into(),
2380 diagnostics: Vec::new(),
2381 imports: Vec::new(),
2382 exports: Vec::new(),
2383 type_aliases: Vec::new(),
2384 local_type_bindings: Vec::new(),
2385 local_value_bindings: Vec::new(),
2386 classes: vec![presolve_parser::ParsedClass {
2387 name: "DuplicateEvent".to_string(),
2388 span: test_span(),
2389 heritage: None,
2390 decorators: vec![presolve_parser::ParsedDecorator {
2391 name: "component".to_string(),
2392 is_invoked: true,
2393 arguments: vec![Some("x-duplicate-event".to_string())],
2394 argument: Some("x-duplicate-event".to_string()),
2395 argument_count: 1,
2396 argument_spans: vec![test_span()],
2397 static_member_argument: None,
2398 this_member_argument: None,
2399 validation_rule_expression: None,
2400 span: test_span(),
2401 }],
2402 properties: Vec::new(),
2403 methods: vec![presolve_parser::ParsedMethod {
2404 name: "render".to_string(),
2405 span: test_span(),
2406 decorators: Vec::new(),
2407 is_getter: false,
2408 is_setter: false,
2409 is_async: false,
2410 is_static: false,
2411 jsx_roots: vec![presolve_parser::ParsedJsxNode::Element(
2412 presolve_parser::ParsedJsxElement {
2413 name: "button".to_string(),
2414 name_span: test_span(),
2415 span: test_span(),
2416 attributes: Vec::new(),
2417 event_handlers: vec![
2418 presolve_parser::ParsedEventHandler {
2419 event: "click".to_string(),
2420 handler: "this.render".to_string(),
2421 arguments: Vec::new(),
2422 span: test_span(),
2423 },
2424 presolve_parser::ParsedEventHandler {
2425 event: "click".to_string(),
2426 handler: "this.render".to_string(),
2427 arguments: Vec::new(),
2428 span: test_span(),
2429 },
2430 ],
2431 children: Vec::new(),
2432 },
2433 )],
2434 bindings: Vec::new(),
2435 state_updates: Vec::new(),
2436 local_variables: Vec::new(),
2437 parameters: Vec::new(),
2438 return_type_annotation: None,
2439 return_values: Vec::new(),
2440 computed_expression: None,
2441 effect_body: None,
2442 calls: Vec::new(),
2443 }],
2444 }],
2445 };
2446
2447 let graph = build_component_graph(&parsed);
2448
2449 assert!(graph
2450 .diagnostics
2451 .iter()
2452 .any(|diagnostic| diagnostic.code == "PSC1006"));
2453 }
2454
2455 fn test_span() -> presolve_parser::SourceSpan {
2456 presolve_parser::SourceSpan {
2457 start: 0,
2458 end: 0,
2459 line: 1,
2460 column: 1,
2461 }
2462 }
2463
2464 #[test]
2465 fn builds_increment_action_from_parsed_method_update() {
2466 let source = include_str!("../../../fixtures/0004-nested-jsx/input/NestedCounter.tsx");
2467
2468 let parsed =
2469 presolve_parser::parse_file("fixtures/0004-nested-jsx/input/NestedCounter.tsx", source);
2470
2471 let graph = build_component_graph(&parsed);
2472 let component = graph.components.first().expect("expected component");
2473
2474 assert_eq!(
2475 component.actions,
2476 vec![ComponentAction {
2477 id: SemanticId::component(Some("x-nested-counter"), "NestedCounter")
2478 .action("increment", 0),
2479 owner: SemanticOwner::entity(
2480 SemanticId::component(Some("x-nested-counter"), "NestedCounter")
2481 .method("increment"),
2482 ),
2483 method: "increment".to_string(),
2484 operation: StateOperation::Increment,
2485 field: "count".to_string(),
2486 }]
2487 );
2488 }
2489
2490 #[test]
2491 fn builds_decrement_action_from_parsed_method_update() {
2492 let source =
2493 include_str!("../../../fixtures/0009-decrement-counter/input/DecrementCounter.tsx");
2494
2495 let parsed = presolve_parser::parse_file(
2496 "fixtures/0009-decrement-counter/input/DecrementCounter.tsx",
2497 source,
2498 );
2499
2500 let graph = build_component_graph(&parsed);
2501 let component = graph.components.first().expect("expected component");
2502
2503 assert_eq!(
2504 component.actions,
2505 vec![ComponentAction {
2506 id: SemanticId::component(Some("x-decrement-counter"), "DecrementCounter")
2507 .action("decrement", 0),
2508 owner: SemanticOwner::entity(
2509 SemanticId::component(Some("x-decrement-counter"), "DecrementCounter")
2510 .method("decrement"),
2511 ),
2512 method: "decrement".to_string(),
2513 operation: StateOperation::Decrement,
2514 field: "count".to_string(),
2515 }]
2516 );
2517 }
2518
2519 #[test]
2520 fn builds_add_and_subtract_assign_actions_from_parsed_method_updates() {
2521 let source =
2522 include_str!("../../../fixtures/0010-add-subtract-assign/input/StepCounter.tsx");
2523
2524 let parsed = presolve_parser::parse_file(
2525 "fixtures/0010-add-subtract-assign/input/StepCounter.tsx",
2526 source,
2527 );
2528
2529 let graph = build_component_graph(&parsed);
2530 let component = graph.components.first().expect("expected component");
2531
2532 assert_eq!(
2533 component.actions,
2534 vec![
2535 ComponentAction {
2536 id: SemanticId::component(Some("x-step-counter"), "StepCounter")
2537 .action("addTwo", 0),
2538 owner: SemanticOwner::entity(
2539 SemanticId::component(Some("x-step-counter"), "StepCounter")
2540 .method("addTwo"),
2541 ),
2542 method: "addTwo".to_string(),
2543 operation: StateOperation::AddAssign(SerializableValue::Number(
2544 "2".to_string()
2545 )),
2546 field: "count".to_string(),
2547 },
2548 ComponentAction {
2549 id: SemanticId::component(Some("x-step-counter"), "StepCounter")
2550 .action("subtractThree", 0),
2551 owner: SemanticOwner::entity(
2552 SemanticId::component(Some("x-step-counter"), "StepCounter")
2553 .method("subtractThree"),
2554 ),
2555 method: "subtractThree".to_string(),
2556 operation: StateOperation::SubtractAssign(SerializableValue::Number(
2557 "3".to_string()
2558 )),
2559 field: "count".to_string(),
2560 }
2561 ]
2562 );
2563 }
2564
2565 #[test]
2566 fn builds_direct_assignment_action_from_parsed_method_update() {
2567 let source =
2568 include_str!("../../../fixtures/0011-direct-assignment/input/ResetCounter.tsx");
2569
2570 let parsed = presolve_parser::parse_file(
2571 "fixtures/0011-direct-assignment/input/ResetCounter.tsx",
2572 source,
2573 );
2574
2575 let graph = build_component_graph(&parsed);
2576 let component = graph.components.first().expect("expected component");
2577
2578 assert_eq!(
2579 component.actions,
2580 vec![ComponentAction {
2581 id: SemanticId::component(Some("x-reset-counter"), "ResetCounter")
2582 .action("reset", 0),
2583 owner: SemanticOwner::entity(
2584 SemanticId::component(Some("x-reset-counter"), "ResetCounter").method("reset"),
2585 ),
2586 method: "reset".to_string(),
2587 operation: StateOperation::Assign(SerializableValue::Number("0".to_string())),
2588 field: "count".to_string(),
2589 }]
2590 );
2591 }
2592
2593 #[test]
2594 fn builds_boolean_toggle_action_from_parsed_method_update() {
2595 let source = include_str!("../../../fixtures/0012-boolean-toggle/input/ToggleFlag.tsx");
2596
2597 let parsed = presolve_parser::parse_file(
2598 "fixtures/0012-boolean-toggle/input/ToggleFlag.tsx",
2599 source,
2600 );
2601
2602 let graph = build_component_graph(&parsed);
2603 let component = graph.components.first().expect("expected component");
2604
2605 assert_eq!(
2606 component.actions,
2607 vec![ComponentAction {
2608 id: SemanticId::component(Some("x-toggle-flag"), "ToggleFlag").action("toggle", 0),
2609 owner: SemanticOwner::entity(
2610 SemanticId::component(Some("x-toggle-flag"), "ToggleFlag").method("toggle"),
2611 ),
2612 method: "toggle".to_string(),
2613 operation: StateOperation::Toggle,
2614 field: "enabled".to_string(),
2615 }]
2616 );
2617 }
2618
2619 #[test]
2620 fn builds_multi_step_actions_from_parsed_method_updates_in_source_order() {
2621 let source =
2622 include_str!("../../../fixtures/0013-multi-step-action/input/BatchActionCounter.tsx");
2623
2624 let parsed = presolve_parser::parse_file(
2625 "fixtures/0013-multi-step-action/input/BatchActionCounter.tsx",
2626 source,
2627 );
2628
2629 let graph = build_component_graph(&parsed);
2630 let component = graph.components.first().expect("expected component");
2631
2632 assert_eq!(
2633 component.actions,
2634 vec![
2635 ComponentAction {
2636 id:
2637 SemanticId::component(Some("x-batch-action-counter"), "BatchActionCounter",)
2638 .action("apply", 0),
2639 owner:
2640 SemanticOwner::entity(
2641 SemanticId::component(
2642 Some("x-batch-action-counter"),
2643 "BatchActionCounter",
2644 )
2645 .method("apply"),
2646 ),
2647 method: "apply".to_string(),
2648 operation: StateOperation::AddAssign(SerializableValue::Number(
2649 "2".to_string()
2650 )),
2651 field: "count".to_string(),
2652 },
2653 ComponentAction {
2654 id:
2655 SemanticId::component(Some("x-batch-action-counter"), "BatchActionCounter",)
2656 .action("apply", 1),
2657 owner:
2658 SemanticOwner::entity(
2659 SemanticId::component(
2660 Some("x-batch-action-counter"),
2661 "BatchActionCounter",
2662 )
2663 .method("apply"),
2664 ),
2665 method: "apply".to_string(),
2666 operation: StateOperation::Decrement,
2667 field: "count".to_string(),
2668 },
2669 ComponentAction {
2670 id:
2671 SemanticId::component(Some("x-batch-action-counter"), "BatchActionCounter",)
2672 .action("apply", 2),
2673 owner:
2674 SemanticOwner::entity(
2675 SemanticId::component(
2676 Some("x-batch-action-counter"),
2677 "BatchActionCounter",
2678 )
2679 .method("apply"),
2680 ),
2681 method: "apply".to_string(),
2682 operation: StateOperation::Assign(SerializableValue::Number("8".to_string())),
2683 field: "count".to_string(),
2684 },
2685 ComponentAction {
2686 id:
2687 SemanticId::component(Some("x-batch-action-counter"), "BatchActionCounter",)
2688 .action("apply", 3),
2689 owner:
2690 SemanticOwner::entity(
2691 SemanticId::component(
2692 Some("x-batch-action-counter"),
2693 "BatchActionCounter",
2694 )
2695 .method("apply"),
2696 ),
2697 method: "apply".to_string(),
2698 operation: StateOperation::Increment,
2699 field: "count".to_string(),
2700 },
2701 ComponentAction {
2702 id:
2703 SemanticId::component(Some("x-batch-action-counter"), "BatchActionCounter",)
2704 .action("apply", 4),
2705 owner:
2706 SemanticOwner::entity(
2707 SemanticId::component(
2708 Some("x-batch-action-counter"),
2709 "BatchActionCounter",
2710 )
2711 .method("apply"),
2712 ),
2713 method: "apply".to_string(),
2714 operation: StateOperation::Toggle,
2715 field: "enabled".to_string(),
2716 }
2717 ]
2718 );
2719 }
2720
2721 #[test]
2722 fn generates_static_html_from_template_graph() {
2723 let source = include_str!("../../../fixtures/0001-source-summary/input/Counter.tsx");
2724
2725 let parsed =
2726 presolve_parser::parse_file("fixtures/0001-source-summary/input/Counter.tsx", source);
2727
2728 let component_graph = build_component_graph(&parsed);
2729 let template_graph = build_template_graph(&component_graph);
2730 let html = generate_static_html(&template_graph);
2731
2732 assert_eq!(
2733 html,
2734 "<button data-presolve-node=\"n0\" data-presolve-on-click=\"this.increment\" data-presolve-bindings=\"this.count\">Count:<!-- presolve-binding:n1:this.count -->0</button>\n"
2735 );
2736 }
2737
2738 #[test]
2739 fn lowers_jsx_html_attribute_aliases_before_html_and_manifest_generation() {
2740 let source = r#"
2741@component("x-attribute-alias")
2742class AttributeAlias extends Component {
2743 target = state("profile-name");
2744 render() {
2745 return <label className={this.target} htmlFor={this.target}>Profile</label>;
2746 }
2747}
2748"#;
2749 let parsed = presolve_parser::parse_file("AttributeAlias.tsx", source);
2750 let component_graph = build_component_graph(&parsed);
2751 let template_graph = build_template_graph(&component_graph);
2752 let html = generate_static_html(&template_graph);
2753 let manifest = build_template_manifest(&component_graph, &template_graph);
2754 let model = build_application_semantic_model_from_component_graph(&component_graph);
2755 let runtime = build_runtime_component_artifact(&model, &model.component_ir_optimization);
2756
2757 assert!(html.contains("class=\"profile-name\""));
2758 assert!(html.contains("for=\"profile-name\""));
2759 assert!(!html.contains("className="));
2760 assert!(!html.contains("htmlFor="));
2761 assert_eq!(
2762 manifest.components[0]
2763 .template
2764 .nodes
2765 .iter()
2766 .filter_map(|node| match node {
2767 ManifestNode::Binding { attribute, .. } => attribute.as_deref(),
2768 _ => None,
2769 })
2770 .collect::<Vec<_>>(),
2771 vec!["class", "for"]
2772 );
2773 assert_eq!(
2774 runtime
2775 .ordinary_template_bindings
2776 .iter()
2777 .filter_map(|binding| binding.attribute_name.as_deref())
2778 .collect::<Vec<_>>(),
2779 vec!["class", "for"]
2780 );
2781 }
2782
2783 #[test]
2784 fn preserves_string_state_literals_in_template_outputs() {
2785 let source = include_str!("../../../fixtures/0006-string-state/input/StringGreeting.tsx");
2786
2787 let parsed = presolve_parser::parse_file(
2788 "fixtures/0006-string-state/input/StringGreeting.tsx",
2789 source,
2790 );
2791
2792 let component_graph = build_component_graph(&parsed);
2793 let component = component_graph
2794 .components
2795 .first()
2796 .expect("expected component");
2797
2798 assert_eq!(
2799 component.state_fields[0].initial_value,
2800 Some(SerializableValue::String("Austin & <Zero>".to_string()))
2801 );
2802
2803 let template_graph = build_template_graph(&component_graph);
2804 let html = generate_static_html(&template_graph);
2805
2806 assert_eq!(
2807 html,
2808 "<p data-presolve-node=\"n0\" data-presolve-bindings=\"this.name\">Name:<!-- presolve-binding:n1:this.name -->Austin & <Zero></p>\n"
2809 );
2810
2811 let manifest = build_template_manifest(&component_graph, &template_graph);
2812
2813 assert_eq!(
2814 manifest.components[0].template.nodes,
2815 vec![
2816 ManifestNode::Element {
2817 id: "n0".to_string(),
2818 tag: "p".to_string(),
2819 },
2820 ManifestNode::Binding {
2821 id: "n1".to_string(),
2822 expression: "this.name".to_string(),
2823 initial_value: Some(SerializableValue::String("Austin & <Zero>".to_string())),
2824 target: None,
2825 element: None,
2826 attribute: None,
2827 }
2828 ]
2829 );
2830
2831 let manifest_json = template_manifest_json(&manifest);
2832 let manifest_value: serde_json::Value =
2833 serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
2834
2835 assert_eq!(
2836 manifest_value["components"][0]["template"]["nodes"][1]["initial_value"],
2837 serde_json::json!("Austin & <Zero>")
2838 );
2839 }
2840
2841 #[test]
2842 fn preserves_static_jsx_attributes_in_template_outputs() {
2843 let source =
2844 include_str!("../../../fixtures/0014-static-attributes/input/StaticAttributePanel.tsx");
2845
2846 let parsed = presolve_parser::parse_file(
2847 "fixtures/0014-static-attributes/input/StaticAttributePanel.tsx",
2848 source,
2849 );
2850
2851 let component_graph = build_component_graph(&parsed);
2852 assert!(component_graph.diagnostics.is_empty());
2853
2854 let template_graph = build_template_graph(&component_graph);
2855 let root = template_graph.templates[0]
2856 .root
2857 .as_ref()
2858 .expect("expected root");
2859
2860 assert_eq!(root.attributes.len(), 3);
2861 assert_eq!(root.attributes[0].name, "id");
2862 assert_eq!(
2863 root.attributes[0].value,
2864 AttributeValue::Static("panel-root".to_string())
2865 );
2866 assert_eq!(root.attributes[1].name, "aria-label");
2867 assert_eq!(
2868 root.attributes[1].value,
2869 AttributeValue::Static("Status \"Panel\"".to_string())
2870 );
2871 assert_eq!(root.attributes[2].name, "hidden");
2872 assert_eq!(root.attributes[2].value, AttributeValue::Boolean);
2873
2874 let TemplateChild::Element(button) = &root.children[0] else {
2875 panic!("expected button child");
2876 };
2877
2878 assert_eq!(button.attributes.len(), 4);
2879 assert_eq!(button.attributes[0].name, "type");
2880 assert_eq!(
2881 button.attributes[0].value,
2882 AttributeValue::Static("button".to_string())
2883 );
2884 assert_eq!(button.attributes[1].name, "data-mode");
2885 assert_eq!(
2886 button.attributes[1].value,
2887 AttributeValue::Static("safe & sound".to_string())
2888 );
2889 assert_eq!(button.attributes[2].name, "title");
2890 assert_eq!(
2891 button.attributes[2].value,
2892 AttributeValue::Static("Use <carefully>".to_string())
2893 );
2894 assert_eq!(button.attributes[3].name, "data-presolve-bindings");
2895
2896 let html = generate_static_html(&template_graph);
2897
2898 assert_eq!(
2899 html,
2900 "<section data-presolve-node=\"n0\" id=\"panel-root\" aria-label=\"Status "Panel"\" hidden><button data-presolve-node=\"n1\" type=\"button\" data-mode=\"safe & sound\" title=\"Use <carefully>\" data-presolve-bindings=\"this.label\">Label:<!-- presolve-binding:n2:this.label -->Ready</button></section>\n"
2901 );
2902 }
2903
2904 #[test]
2905 fn reports_static_attribute_semantic_errors() {
2906 let source = r#"
2907@component("x-bad-attrs")
2908class BadAttrs extends Component {
2909 disabled = state(false);
2910
2911 render() {
2912 return <button type="button" type="submit" title={label} {...props}>Go</button>;
2913 }
2914}
2915"#;
2916
2917 let parsed = presolve_parser::parse_file("BadAttrs.tsx", source);
2918 let graph = build_component_graph(&parsed);
2919 let codes = graph
2920 .diagnostics
2921 .iter()
2922 .map(|diagnostic| diagnostic.code.as_str())
2923 .collect::<Vec<_>>();
2924
2925 assert!(codes.contains(&"PSC1007"));
2926 assert!(codes.contains(&"PSC1008"));
2927 assert!(codes.contains(&"PSC1009"));
2928 }
2929
2930 #[test]
2931 fn builds_dynamic_attribute_bindings_in_template_outputs() {
2932 let source = include_str!(
2933 "../../../fixtures/0015-dynamic-attributes/input/DynamicAttributeButton.tsx"
2934 );
2935
2936 let parsed = presolve_parser::parse_file(
2937 "fixtures/0015-dynamic-attributes/input/DynamicAttributeButton.tsx",
2938 source,
2939 );
2940
2941 let component_graph = build_component_graph(&parsed);
2942 assert!(component_graph.diagnostics.is_empty());
2943
2944 let template_graph = build_template_graph(&component_graph);
2945 let root = template_graph.templates[0]
2946 .root
2947 .as_ref()
2948 .expect("expected root");
2949
2950 assert_eq!(root.attributes[0].name, "disabled");
2951 assert_eq!(
2952 root.attributes[0].value,
2953 AttributeValue::Binding {
2954 id: TemplateNodeId("n1".to_string()),
2955 expression: "this.disabled".to_string(),
2956 initial_value: Some(SerializableValue::Boolean(false)),
2957 }
2958 );
2959 assert_eq!(root.attributes[1].name, "title");
2960 assert_eq!(
2961 root.attributes[1].value,
2962 AttributeValue::Binding {
2963 id: TemplateNodeId("n2".to_string()),
2964 expression: "this.label".to_string(),
2965 initial_value: Some(SerializableValue::String("Ready".to_string())),
2966 }
2967 );
2968
2969 let html = generate_static_html(&template_graph);
2970
2971 assert_eq!(
2972 html,
2973 "<button data-presolve-node=\"n0\" title=\"Ready\" data-presolve-on-click=\"this.lock\" data-presolve-bindings=\"this.label\">Status:<!-- presolve-binding:n3:this.label -->Ready</button>\n"
2974 );
2975 }
2976
2977 #[test]
2978 fn preserves_boolean_state_literals_in_template_outputs() {
2979 let source = include_str!("../../../fixtures/0007-boolean-state/input/BooleanFlags.tsx");
2980
2981 let parsed = presolve_parser::parse_file(
2982 "fixtures/0007-boolean-state/input/BooleanFlags.tsx",
2983 source,
2984 );
2985
2986 let component_graph = build_component_graph(&parsed);
2987 let component = component_graph
2988 .components
2989 .first()
2990 .expect("expected component");
2991
2992 assert_eq!(
2993 component.state_fields[0].initial_value,
2994 Some(SerializableValue::Boolean(true))
2995 );
2996 assert_eq!(
2997 component.state_fields[1].initial_value,
2998 Some(SerializableValue::Boolean(false))
2999 );
3000
3001 let template_graph = build_template_graph(&component_graph);
3002 let html = generate_static_html(&template_graph);
3003
3004 assert_eq!(
3005 html,
3006 "<section data-presolve-node=\"n0\"><p data-presolve-node=\"n1\" data-presolve-bindings=\"this.enabled\">Enabled:<!-- presolve-binding:n2:this.enabled -->true</p><p data-presolve-node=\"n3\" data-presolve-bindings=\"this.disabled\">Disabled:<!-- presolve-binding:n4:this.disabled -->false</p></section>\n"
3007 );
3008
3009 let manifest = build_template_manifest(&component_graph, &template_graph);
3010
3011 assert_eq!(
3012 manifest.components[0].template.nodes,
3013 vec![
3014 ManifestNode::Element {
3015 id: "n0".to_string(),
3016 tag: "section".to_string(),
3017 },
3018 ManifestNode::Element {
3019 id: "n1".to_string(),
3020 tag: "p".to_string(),
3021 },
3022 ManifestNode::Binding {
3023 id: "n2".to_string(),
3024 expression: "this.enabled".to_string(),
3025 initial_value: Some(SerializableValue::Boolean(true)),
3026 target: None,
3027 element: None,
3028 attribute: None,
3029 },
3030 ManifestNode::Element {
3031 id: "n3".to_string(),
3032 tag: "p".to_string(),
3033 },
3034 ManifestNode::Binding {
3035 id: "n4".to_string(),
3036 expression: "this.disabled".to_string(),
3037 initial_value: Some(SerializableValue::Boolean(false)),
3038 target: None,
3039 element: None,
3040 attribute: None,
3041 }
3042 ]
3043 );
3044
3045 let manifest_json = template_manifest_json(&manifest);
3046 let manifest_value: serde_json::Value =
3047 serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
3048
3049 assert_eq!(
3050 manifest_value["components"][0]["template"]["nodes"][2]["initial_value"],
3051 serde_json::json!(true)
3052 );
3053 assert_eq!(
3054 manifest_value["components"][0]["template"]["nodes"][4]["initial_value"],
3055 serde_json::json!(false)
3056 );
3057 }
3058
3059 #[test]
3060 fn preserves_null_state_literals_in_template_outputs() {
3061 let source = include_str!("../../../fixtures/0008-null-state/input/NullSelection.tsx");
3062
3063 let parsed =
3064 presolve_parser::parse_file("fixtures/0008-null-state/input/NullSelection.tsx", source);
3065
3066 let component_graph = build_component_graph(&parsed);
3067 let component = component_graph
3068 .components
3069 .first()
3070 .expect("expected component");
3071
3072 assert_eq!(
3073 component.state_fields[0].initial_value,
3074 Some(SerializableValue::Null)
3075 );
3076
3077 let template_graph = build_template_graph(&component_graph);
3078 let html = generate_static_html(&template_graph);
3079
3080 assert_eq!(
3081 html,
3082 "<p data-presolve-node=\"n0\" data-presolve-bindings=\"this.selection\">Selection:<!-- presolve-binding:n1:this.selection --></p>\n"
3083 );
3084
3085 let manifest = build_template_manifest(&component_graph, &template_graph);
3086
3087 assert_eq!(
3088 manifest.components[0].template.nodes,
3089 vec![
3090 ManifestNode::Element {
3091 id: "n0".to_string(),
3092 tag: "p".to_string(),
3093 },
3094 ManifestNode::Binding {
3095 id: "n1".to_string(),
3096 expression: "this.selection".to_string(),
3097 initial_value: Some(SerializableValue::Null),
3098 target: None,
3099 element: None,
3100 attribute: None,
3101 }
3102 ]
3103 );
3104
3105 let manifest_json = template_manifest_json(&manifest);
3106 let manifest_value: serde_json::Value =
3107 serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
3108
3109 assert_eq!(
3110 manifest_value["components"][0]["template"]["nodes"][1]["initial_value"],
3111 serde_json::Value::Null
3112 );
3113 }
3114
3115 #[test]
3116 fn builds_template_graph_from_component_graph() {
3117 let source = include_str!("../../../fixtures/0001-source-summary/input/Counter.tsx");
3118
3119 let parsed =
3120 presolve_parser::parse_file("fixtures/0001-source-summary/input/Counter.tsx", source);
3121
3122 let component_graph = build_component_graph(&parsed);
3123 let template_graph = build_template_graph(&component_graph);
3124
3125 assert_eq!(template_graph.templates.len(), 1);
3126
3127 let template = &template_graph.templates[0];
3128 assert_eq!(template.component_name, "Counter");
3129 assert_eq!(template.id.as_str(), "component:x-counter/template:render");
3130 assert_eq!(
3131 template.provenance.path,
3132 Path::new("fixtures/0001-source-summary/input/Counter.tsx")
3133 );
3134 assert_eq!(template.provenance.span.line, 10);
3135 assert_eq!(
3136 template.owner,
3137 SemanticOwner::entity(SemanticId::component(Some("x-counter"), "Counter"))
3138 );
3139
3140 let root = template.root.as_ref().expect("expected template root");
3141
3142 assert_eq!(root.id.0, "n0");
3143 assert_eq!(root.tag_name, "button");
3144 assert_eq!(root.span.line, 12);
3145 assert_eq!(root.span.column, 7);
3146
3147 assert_eq!(root.attributes.len(), 2);
3148 assert_eq!(root.attributes[0].name, "data-presolve-on-click");
3149 assert_eq!(
3150 root.attributes[0].span.expect("expected event span").line,
3151 12
3152 );
3153 assert_eq!(
3154 root.attributes[0].span.expect("expected event span").column,
3155 15
3156 );
3157 assert_eq!(
3158 root.attributes[0].value,
3159 AttributeValue::EventHandler {
3160 event: "click".to_string(),
3161 handler: "this.increment".to_string(),
3162 arguments: Vec::new(),
3163 }
3164 );
3165
3166 assert_eq!(root.attributes[1].name, "data-presolve-bindings");
3167 assert_eq!(root.attributes[1].span, None);
3168 assert_eq!(
3169 root.attributes[1].value,
3170 AttributeValue::BindingList(vec!["this.count".to_string()])
3171 );
3172
3173 assert_eq!(root.children.len(), 2);
3174 let TemplateChild::Text { value, span } = &root.children[0] else {
3175 panic!("expected text child");
3176 };
3177 assert_eq!(value, "Count:");
3178 assert_eq!(span.line, 13);
3179 assert_eq!(span.column, 9);
3180
3181 let TemplateChild::Binding {
3182 id,
3183 expression,
3184 initial_value,
3185 span,
3186 } = &root.children[1]
3187 else {
3188 panic!("expected binding child");
3189 };
3190 assert_eq!(id.0, "n1");
3191 assert_eq!(expression, "this.count");
3192 assert_eq!(
3193 initial_value,
3194 &Some(SerializableValue::Number("0".to_string()))
3195 );
3196 assert_eq!(span.line, 13);
3197 assert_eq!(span.column, 16);
3198 }
3199
3200 #[test]
3201 fn semantic_ids_are_stable_when_component_declaration_order_changes() {
3202 let alpha = r#"
3203@component("x-alpha")
3204class Alpha extends Component {
3205 count = state(0);
3206
3207 increment() {
3208 this.count++;
3209 }
3210
3211 render() {
3212 return <p>{this.count}</p>;
3213 }
3214}
3215"#;
3216 let beta = r#"
3217@component("x-beta")
3218class Beta extends Component {
3219 enabled = state(false);
3220
3221 toggle() {
3222 this.enabled = !this.enabled;
3223 }
3224
3225 render() {
3226 return <p>{this.enabled}</p>;
3227 }
3228}
3229"#;
3230
3231 let ids_for = |source: &str| {
3232 let parsed = presolve_parser::parse_file("App.tsx", source);
3233 let component_graph = build_component_graph(&parsed);
3234 let template_graph = build_template_graph(&component_graph);
3235 let template_ids = template_graph
3236 .templates
3237 .iter()
3238 .map(|template| (template.component_name.as_str(), template.id.to_string()))
3239 .collect::<BTreeMap<_, _>>();
3240
3241 component_graph
3242 .components
3243 .iter()
3244 .map(|component| {
3245 let ids = component
3246 .state_fields
3247 .iter()
3248 .map(|field| field.id.to_string())
3249 .chain(component.methods.iter().map(|method| method.id.to_string()))
3250 .chain(component.actions.iter().map(|action| action.id.to_string()))
3251 .chain(std::iter::once(
3252 template_ids
3253 .get(component.class_name.as_str())
3254 .expect("expected component template")
3255 .clone(),
3256 ))
3257 .collect::<Vec<_>>();
3258
3259 (
3260 component.class_name.clone(),
3261 (component.id.to_string(), ids),
3262 )
3263 })
3264 .collect::<BTreeMap<_, _>>()
3265 };
3266
3267 assert_eq!(
3268 ids_for(&format!("{alpha}\n{beta}")),
3269 ids_for(&format!("{beta}\n{alpha}"))
3270 );
3271 }
3272
3273 #[test]
3274 fn carries_source_spans_into_nested_template_nodes() {
3275 let source = include_str!("../../../fixtures/0004-nested-jsx/input/NestedCounter.tsx");
3276
3277 let parsed =
3278 presolve_parser::parse_file("fixtures/0004-nested-jsx/input/NestedCounter.tsx", source);
3279
3280 let component_graph = build_component_graph(&parsed);
3281 let template_graph = build_template_graph(&component_graph);
3282 let root = template_graph.templates[0]
3283 .root
3284 .as_ref()
3285 .expect("expected root");
3286
3287 assert_eq!(root.tag_name, "section");
3288 assert_eq!(root.span.line, 12);
3289 assert_eq!(root.span.column, 7);
3290
3291 let TemplateChild::Element(button) = &root.children[0] else {
3292 panic!("expected nested button");
3293 };
3294
3295 assert_eq!(button.tag_name, "button");
3296 assert_eq!(button.span.line, 13);
3297 assert_eq!(button.span.column, 9);
3298 assert_eq!(button.attributes[0].name, "data-presolve-on-click");
3299 assert_eq!(
3300 button.attributes[0].span.expect("expected event span").line,
3301 13
3302 );
3303 assert_eq!(
3304 button.attributes[0]
3305 .span
3306 .expect("expected event span")
3307 .column,
3308 17
3309 );
3310 assert_eq!(button.attributes[1].name, "data-presolve-bindings");
3311 assert_eq!(button.attributes[1].span, None);
3312
3313 let TemplateChild::Text { value, span } = &button.children[0] else {
3314 panic!("expected nested text");
3315 };
3316 assert_eq!(value, "Count:");
3317 assert_eq!(span.line, 13);
3318 assert_eq!(span.column, 50);
3319
3320 let TemplateChild::Binding {
3321 expression, span, ..
3322 } = &button.children[1]
3323 else {
3324 panic!("expected nested binding");
3325 };
3326 assert_eq!(expression, "this.count");
3327 assert_eq!(span.line, 13);
3328 assert_eq!(span.column, 57);
3329 }
3330
3331 #[test]
3332 fn preserves_fragment_siblings_without_wrapper_elements() {
3333 let source = include_str!("../../../fixtures/0016-fragments/input/FragmentPanel.tsx");
3334
3335 let parsed =
3336 presolve_parser::parse_file("fixtures/0016-fragments/input/FragmentPanel.tsx", source);
3337
3338 let component_graph = build_component_graph(&parsed);
3339 assert!(component_graph.diagnostics.is_empty());
3340
3341 let template_graph = build_template_graph(&component_graph);
3342 let template = &template_graph.templates[0];
3343 let fragment = template
3344 .root_fragment
3345 .as_ref()
3346 .expect("expected fragment root");
3347
3348 assert!(template.root.is_none());
3349 assert_eq!(fragment.id.0, "n0");
3350 assert_eq!(fragment.children.len(), 2);
3351
3352 let TemplateChild::Element(heading) = &fragment.children[0] else {
3353 panic!("expected heading child");
3354 };
3355 assert_eq!(heading.id.0, "n1");
3356 assert_eq!(heading.tag_name, "h1");
3357
3358 let TemplateChild::Fragment(nested) = &fragment.children[1] else {
3359 panic!("expected nested fragment child");
3360 };
3361 assert_eq!(nested.id.0, "n2");
3362
3363 let TemplateChild::Element(paragraph) = &nested.children[0] else {
3364 panic!("expected paragraph child");
3365 };
3366 assert_eq!(paragraph.id.0, "n3");
3367 assert_eq!(paragraph.tag_name, "p");
3368
3369 let html = generate_static_html(&template_graph);
3370 assert_eq!(
3371 html,
3372 "<h1 data-presolve-node=\"n1\">Title</h1><p data-presolve-node=\"n3\" data-presolve-bindings=\"this.label\">Status:<!-- presolve-binding:n4:this.label -->Ready</p><span data-presolve-node=\"n5\">Done</span>\n"
3373 );
3374
3375 let manifest = build_template_manifest(&component_graph, &template_graph);
3376 assert_eq!(
3377 manifest.components[0].template.nodes,
3378 vec![
3379 ManifestNode::Element {
3380 id: "n1".to_string(),
3381 tag: "h1".to_string(),
3382 },
3383 ManifestNode::Element {
3384 id: "n3".to_string(),
3385 tag: "p".to_string(),
3386 },
3387 ManifestNode::Binding {
3388 id: "n4".to_string(),
3389 expression: "this.label".to_string(),
3390 initial_value: Some(SerializableValue::String("Ready".to_string())),
3391 target: None,
3392 element: None,
3393 attribute: None,
3394 },
3395 ManifestNode::Element {
3396 id: "n5".to_string(),
3397 tag: "span".to_string(),
3398 },
3399 ]
3400 );
3401 }
3402
3403 #[test]
3404 fn builds_conditional_template_boundaries_and_manifest_branch_html() {
3405 let source = include_str!(
3406 "../../../fixtures/0017-conditional-rendering/input/ConditionalStatus.tsx"
3407 );
3408
3409 let parsed = presolve_parser::parse_file(
3410 "fixtures/0017-conditional-rendering/input/ConditionalStatus.tsx",
3411 source,
3412 );
3413
3414 let component_graph = build_component_graph(&parsed);
3415 assert!(component_graph.diagnostics.is_empty());
3416
3417 let template_graph = build_template_graph(&component_graph);
3418 let root = template_graph.templates[0]
3419 .root
3420 .as_ref()
3421 .expect("expected root element");
3422
3423 assert_eq!(root.id.0, "n0");
3424 assert_eq!(root.attributes[1].name, "data-presolve-bindings");
3425
3426 let TemplateChild::Conditional(conditional) = &root.children[0] else {
3427 panic!("expected conditional child");
3428 };
3429
3430 assert_eq!(conditional.id.0, "n1");
3431 assert_eq!(conditional.start_id.0, "n2");
3432 assert_eq!(conditional.end_id.0, "n3");
3433 assert_eq!(conditional.condition, "this.enabled");
3434 assert_eq!(
3435 conditional.initial_value,
3436 Some(SerializableValue::Boolean(true))
3437 );
3438
3439 let TemplateChild::Element(when_true) = &conditional.when_true[0] else {
3440 panic!("expected true branch element");
3441 };
3442 assert_eq!(when_true.id.0, "n4");
3443 assert_eq!(when_true.tag_name, "span");
3444
3445 let TemplateChild::Element(when_false) = &conditional.when_false[0] else {
3446 panic!("expected false branch element");
3447 };
3448 assert_eq!(when_false.id.0, "n5");
3449 assert_eq!(when_false.tag_name, "span");
3450
3451 let html = generate_static_html(&template_graph);
3452 assert_eq!(
3453 html,
3454 "<button data-presolve-node=\"n0\" data-presolve-on-click=\"this.toggle\" data-presolve-bindings=\"this.enabled\"><!-- presolve-conditional-start:n2:this.enabled --><span data-presolve-node=\"n4\">On</span><!-- presolve-conditional-end:n3 --></button>\n"
3455 );
3456
3457 let manifest = build_template_manifest(&component_graph, &template_graph);
3458 assert_eq!(
3459 manifest.components[0].template.nodes,
3460 vec![
3461 ManifestNode::Element {
3462 id: "n0".to_string(),
3463 tag: "button".to_string(),
3464 },
3465 ManifestNode::Conditional {
3466 id: "n1".to_string(),
3467 start: "n2".to_string(),
3468 end: "n3".to_string(),
3469 condition: "this.enabled".to_string(),
3470 initial_value: Some(SerializableValue::Boolean(true)),
3471 when_true_html: "<span data-presolve-node=\"n4\">On</span>".to_string(),
3472 when_false_html: "<span data-presolve-node=\"n5\">Off</span>".to_string(),
3473 },
3474 ]
3475 );
3476 }
3477
3478 #[test]
3479 fn builds_logical_and_conditional_with_empty_false_branch() {
3480 let source = include_str!(
3481 "../../../fixtures/0018-logical-and-conditional/input/LogicalAndStatus.tsx"
3482 );
3483
3484 let parsed = presolve_parser::parse_file(
3485 "fixtures/0018-logical-and-conditional/input/LogicalAndStatus.tsx",
3486 source,
3487 );
3488
3489 let component_graph = build_component_graph(&parsed);
3490 assert!(component_graph.diagnostics.is_empty());
3491
3492 let template_graph = build_template_graph(&component_graph);
3493 let root = template_graph.templates[0]
3494 .root
3495 .as_ref()
3496 .expect("expected root element");
3497
3498 let TemplateChild::Conditional(conditional) = &root.children[0] else {
3499 panic!("expected conditional child");
3500 };
3501
3502 assert_eq!(conditional.when_true.len(), 1);
3503 assert!(conditional.when_false.is_empty());
3504
3505 let manifest = build_template_manifest(&component_graph, &template_graph);
3506 assert_eq!(
3507 manifest.components[0].template.nodes[1],
3508 ManifestNode::Conditional {
3509 id: "n1".to_string(),
3510 start: "n2".to_string(),
3511 end: "n3".to_string(),
3512 condition: "this.enabled".to_string(),
3513 initial_value: Some(SerializableValue::Boolean(true)),
3514 when_true_html: "<span data-presolve-node=\"n4\">On</span>".to_string(),
3515 when_false_html: String::new(),
3516 }
3517 );
3518 }
3519
3520 #[test]
3521 fn builds_empty_keyed_list_from_a_serializable_array() {
3522 let source =
3523 include_str!("../../../fixtures/0019-keyed-list-semantics/input/KeyedList.tsx");
3524
3525 let parsed = presolve_parser::parse_file(
3526 "fixtures/0019-keyed-list-semantics/input/KeyedList.tsx",
3527 source,
3528 );
3529
3530 let component_graph = build_component_graph(&parsed);
3531 assert!(component_graph.diagnostics.is_empty());
3532
3533 let template_graph = build_template_graph(&component_graph);
3534 let root = template_graph.templates[0]
3535 .root
3536 .as_ref()
3537 .expect("expected root element");
3538
3539 assert_eq!(root.id.0, "n0");
3540 assert_eq!(root.attributes[0].name, "data-presolve-bindings");
3541
3542 let TemplateChild::List(list) = &root.children[0] else {
3543 panic!("expected keyed list child");
3544 };
3545
3546 assert_eq!(list.id.0, "n1");
3547 assert_eq!(list.start_id.0, "n2");
3548 assert_eq!(list.end_id.0, "n3");
3549 assert_eq!(list.iterable, "this.items");
3550 assert_eq!(
3551 list.initial_value,
3552 Some(SerializableValue::Array(Vec::new()))
3553 );
3554 assert_eq!(list.item_variable, "item");
3555 assert_eq!(list.index_variable.as_deref(), Some("index"));
3556 assert_eq!(list.key_expression, "item.id");
3557
3558 let TemplateChild::Element(item_template) = &list.item_template[0] else {
3559 panic!("expected list item element");
3560 };
3561 assert_eq!(item_template.id.0, "n4");
3562 assert_eq!(item_template.tag_name, "li");
3563
3564 assert_eq!(
3565 generate_static_html(&template_graph),
3566 "<ul data-presolve-node=\"n0\" data-presolve-bindings=\"this.items\"><!-- presolve-list-start:n2:this.items --><!-- presolve-list-end:n3 --></ul>\n"
3567 );
3568
3569 let manifest = build_template_manifest(&component_graph, &template_graph);
3570 assert_eq!(
3571 manifest.components[0].template.nodes,
3572 vec![
3573 ManifestNode::Element {
3574 id: "n0".to_string(),
3575 tag: "ul".to_string(),
3576 },
3577 ManifestNode::List {
3578 id: "n1".to_string(),
3579 start: "n2".to_string(),
3580 end: "n3".to_string(),
3581 iterable: "this.items".to_string(),
3582 initial_value: Some(SerializableValue::Array(Vec::new())),
3583 item_variable: "item".to_string(),
3584 index_variable: Some("index".to_string()),
3585 key_expression: "item.id".to_string(),
3586 item_root: "n4".to_string(),
3587 item_template_html: "<li data-presolve-node=\"n4:__ez_list_key__\" data-presolve-bindings=\"index,item.label\"><!-- presolve-binding:n5:__ez_list_key__:index -->__ez_list_index__<!-- presolve-list-binding-end:n5:__ez_list_key__ -->:<!-- presolve-binding:n6:__ez_list_key__:item.label --><!-- presolve-list-binding-end:n6:__ez_list_key__ --></li>".to_string(),
3588 },
3589 ]
3590 );
3591 }
3592
3593 #[test]
3594 fn preserves_recursive_object_values_in_component_and_manifest_models() {
3595 let source = include_str!(
3596 "../../../fixtures/0023-recursive-object-values/input/RecursiveObjectValues.tsx"
3597 );
3598 let parsed = presolve_parser::parse_file(
3599 "fixtures/0023-recursive-object-values/input/RecursiveObjectValues.tsx",
3600 source,
3601 );
3602
3603 let component_graph = build_component_graph(&parsed);
3604 assert!(component_graph.diagnostics.is_empty());
3605
3606 let profile = SerializableValue::Object(BTreeMap::from([
3607 (
3608 "name".to_string(),
3609 SerializableValue::String("North".to_string()),
3610 ),
3611 (
3612 "settings".to_string(),
3613 SerializableValue::Object(BTreeMap::from([
3614 ("enabled".to_string(), SerializableValue::Boolean(true)),
3615 (
3616 "tags".to_string(),
3617 SerializableValue::Array(vec![
3618 SerializableValue::String("compiler".to_string()),
3619 SerializableValue::Object(BTreeMap::from([
3620 (
3621 "name".to_string(),
3622 SerializableValue::String("runtime".to_string()),
3623 ),
3624 (
3625 "rank".to_string(),
3626 SerializableValue::Number("2".to_string()),
3627 ),
3628 ])),
3629 ]),
3630 ),
3631 ])),
3632 ),
3633 ]));
3634 assert_eq!(
3635 component_graph.components[0].state_fields[0].initial_value,
3636 Some(profile.clone())
3637 );
3638
3639 let template_graph = build_template_graph(&component_graph);
3640 let manifest = build_template_manifest(&component_graph, &template_graph);
3641 assert_eq!(
3642 manifest.components[0].template.nodes[3],
3643 ManifestNode::Binding {
3644 id: "n3".to_string(),
3645 expression: "this.profile".to_string(),
3646 initial_value: Some(profile),
3647 target: None,
3648 element: None,
3649 attribute: None,
3650 }
3651 );
3652 assert!(matches!(
3653 manifest.components[0].actions[0].operand,
3654 Some(SerializableValue::Object(_))
3655 ));
3656 }
3657
3658 #[test]
3659 fn renders_static_object_list_members_and_keys() {
3660 let source = include_str!(
3661 "../../../fixtures/0024-static-object-keyed-list/input/StaticObjectKeyedList.tsx"
3662 );
3663 let parsed = presolve_parser::parse_file(
3664 "fixtures/0024-static-object-keyed-list/input/StaticObjectKeyedList.tsx",
3665 source,
3666 );
3667 let component_graph = build_component_graph(&parsed);
3668 assert!(component_graph.diagnostics.is_empty());
3669
3670 let template_graph = build_template_graph(&component_graph);
3671 assert_eq!(
3672 generate_static_html(&template_graph),
3673 "<ol data-presolve-node=\"n0\" data-presolve-bindings=\"this.items\"><!-- presolve-list-start:n2:this.items --><li data-presolve-node=\"n4:north\" data-presolve-bindings=\"index,item.label,item.details.region\"><!-- presolve-binding:n5:north:index -->0<!-- presolve-list-binding-end:n5:north -->:<!-- presolve-binding:n6:north:item.label -->North<!-- presolve-list-binding-end:n6:north -->(<!-- presolve-binding:n7:north:item.details.region -->west<!-- presolve-list-binding-end:n7:north -->)</li><li data-presolve-node=\"n4:south\" data-presolve-bindings=\"index,item.label,item.details.region\"><!-- presolve-binding:n5:south:index -->1<!-- presolve-list-binding-end:n5:south -->:<!-- presolve-binding:n6:south:item.label -->South<!-- presolve-list-binding-end:n6:south -->(<!-- presolve-binding:n7:south:item.details.region -->east<!-- presolve-list-binding-end:n7:south -->)</li><!-- presolve-list-end:n3 --></ol>\n"
3674 );
3675 }
3676
3677 #[test]
3678 fn renders_initial_keyed_list_items_from_a_serializable_array() {
3679 let source =
3680 include_str!("../../../fixtures/0020-static-keyed-list/input/StaticKeyedList.tsx");
3681
3682 let parsed = presolve_parser::parse_file(
3683 "fixtures/0020-static-keyed-list/input/StaticKeyedList.tsx",
3684 source,
3685 );
3686 let component_graph = build_component_graph(&parsed);
3687 assert!(component_graph.diagnostics.is_empty());
3688
3689 let template_graph = build_template_graph(&component_graph);
3690 let root = template_graph.templates[0]
3691 .root
3692 .as_ref()
3693 .expect("expected root element");
3694 let TemplateChild::List(list) = &root.children[0] else {
3695 panic!("expected keyed list child");
3696 };
3697
3698 assert_eq!(
3699 list.initial_value,
3700 Some(SerializableValue::Array(vec![
3701 SerializableValue::String("North".to_string()),
3702 SerializableValue::String("South".to_string()),
3703 ]))
3704 );
3705
3706 assert_eq!(
3707 generate_static_html(&template_graph),
3708 "<ol data-presolve-node=\"n0\" data-presolve-bindings=\"this.labels\"><!-- presolve-list-start:n2:this.labels --><li data-presolve-node=\"n4:North\" data-presolve-bindings=\"index,label\"><!-- presolve-binding:n5:North:index -->0<!-- presolve-list-binding-end:n5:North -->:<!-- presolve-binding:n6:North:label -->North<!-- presolve-list-binding-end:n6:North --></li><li data-presolve-node=\"n4:South\" data-presolve-bindings=\"index,label\"><!-- presolve-binding:n5:South:index -->1<!-- presolve-list-binding-end:n5:South -->:<!-- presolve-binding:n6:South:label -->South<!-- presolve-list-binding-end:n6:South --></li><!-- presolve-list-end:n3 --></ol>\n"
3709 );
3710
3711 let manifest = build_template_manifest(&component_graph, &template_graph);
3712 assert_eq!(
3713 manifest.components[0].template.nodes,
3714 vec![
3715 ManifestNode::Element {
3716 id: "n0".to_string(),
3717 tag: "ol".to_string(),
3718 },
3719 ManifestNode::List {
3720 id: "n1".to_string(),
3721 start: "n2".to_string(),
3722 end: "n3".to_string(),
3723 iterable: "this.labels".to_string(),
3724 initial_value: Some(SerializableValue::Array(vec![
3725 SerializableValue::String("North".to_string()),
3726 SerializableValue::String("South".to_string()),
3727 ])),
3728 item_variable: "label".to_string(),
3729 index_variable: Some("index".to_string()),
3730 key_expression: "label".to_string(),
3731 item_root: "n4".to_string(),
3732 item_template_html: "<li data-presolve-node=\"n4:__ez_list_key__\" data-presolve-bindings=\"index,label\"><!-- presolve-binding:n5:__ez_list_key__:index -->__ez_list_index__<!-- presolve-list-binding-end:n5:__ez_list_key__ -->:<!-- presolve-binding:n6:__ez_list_key__:label -->__ez_list_item__<!-- presolve-list-binding-end:n6:__ez_list_key__ --></li>".to_string(),
3733 },
3734 ]
3735 );
3736 }
3737
3738 #[test]
3739 fn builds_template_manifest_for_nested_jsx() {
3740 let source = include_str!("../../../fixtures/0004-nested-jsx/input/NestedCounter.tsx");
3741
3742 let parsed =
3743 presolve_parser::parse_file("fixtures/0004-nested-jsx/input/NestedCounter.tsx", source);
3744
3745 let component_graph = build_component_graph(&parsed);
3746 let template_graph = build_template_graph(&component_graph);
3747 let manifest = build_template_manifest(&component_graph, &template_graph);
3748
3749 assert_eq!(manifest.schema_version, 1);
3750 assert_eq!(manifest.components.len(), 1);
3751
3752 let component = &manifest.components[0];
3753 assert_eq!(component.name, "NestedCounter");
3754
3755 assert_eq!(
3756 component.template.nodes,
3757 vec![
3758 ManifestNode::Element {
3759 id: "n0".to_string(),
3760 tag: "section".to_string(),
3761 },
3762 ManifestNode::Element {
3763 id: "n1".to_string(),
3764 tag: "button".to_string(),
3765 },
3766 ManifestNode::Binding {
3767 id: "n2".to_string(),
3768 expression: "this.count".to_string(),
3769 initial_value: Some(SerializableValue::Number("0".to_string())),
3770 target: None,
3771 element: None,
3772 attribute: None,
3773 }
3774 ]
3775 );
3776
3777 assert_eq!(
3778 component.template.events,
3779 vec![ManifestEvent {
3780 node: "n1".to_string(),
3781 kind: None,
3782 event: "click".to_string(),
3783 handler: "this.increment".to_string(),
3784 arguments: Vec::new(),
3785 method_id: None,
3786 action_batch_id: None,
3787 }]
3788 );
3789
3790 assert_eq!(
3791 component.actions,
3792 vec![ManifestAction {
3793 method: "increment".to_string(),
3794 method_id: None,
3795 action_batch_id: None,
3796 operation: ManifestOperation::Increment,
3797 field: "count".to_string(),
3798 storage_id: None,
3799 operand: None,
3800 }]
3801 );
3802
3803 let manifest_json = template_manifest_json(&manifest);
3804 let manifest_value: serde_json::Value =
3805 serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
3806
3807 assert_eq!(manifest_value["schema_version"], serde_json::json!(1));
3808 }
3809
3810 #[test]
3811 fn builds_template_manifest_for_decrement_action() {
3812 let source =
3813 include_str!("../../../fixtures/0009-decrement-counter/input/DecrementCounter.tsx");
3814
3815 let parsed = presolve_parser::parse_file(
3816 "fixtures/0009-decrement-counter/input/DecrementCounter.tsx",
3817 source,
3818 );
3819
3820 let component_graph = build_component_graph(&parsed);
3821 let template_graph = build_template_graph(&component_graph);
3822 let manifest = build_template_manifest(&component_graph, &template_graph);
3823
3824 assert_eq!(
3825 manifest.components[0].actions,
3826 vec![ManifestAction {
3827 method: "decrement".to_string(),
3828 method_id: None,
3829 action_batch_id: None,
3830 operation: ManifestOperation::Decrement,
3831 field: "count".to_string(),
3832 storage_id: None,
3833 operand: None,
3834 }]
3835 );
3836
3837 let manifest_json = template_manifest_json(&manifest);
3838 let manifest_value: serde_json::Value =
3839 serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
3840
3841 assert_eq!(
3842 manifest_value["components"][0]["actions"][0]["operation"],
3843 serde_json::json!("decrement")
3844 );
3845 }
3846
3847 #[test]
3848 fn builds_template_manifest_for_add_and_subtract_assign_actions() {
3849 let source =
3850 include_str!("../../../fixtures/0010-add-subtract-assign/input/StepCounter.tsx");
3851
3852 let parsed = presolve_parser::parse_file(
3853 "fixtures/0010-add-subtract-assign/input/StepCounter.tsx",
3854 source,
3855 );
3856
3857 let component_graph = build_component_graph(&parsed);
3858 let template_graph = build_template_graph(&component_graph);
3859 let manifest = build_template_manifest(&component_graph, &template_graph);
3860
3861 assert_eq!(
3862 manifest.components[0].actions,
3863 vec![
3864 ManifestAction {
3865 method: "addTwo".to_string(),
3866 method_id: None,
3867 action_batch_id: None,
3868 operation: ManifestOperation::AddAssign,
3869 field: "count".to_string(),
3870 storage_id: None,
3871 operand: Some(SerializableValue::Number("2".to_string())),
3872 },
3873 ManifestAction {
3874 method: "subtractThree".to_string(),
3875 method_id: None,
3876 action_batch_id: None,
3877 operation: ManifestOperation::SubtractAssign,
3878 field: "count".to_string(),
3879 storage_id: None,
3880 operand: Some(SerializableValue::Number("3".to_string())),
3881 }
3882 ]
3883 );
3884
3885 let manifest_json = template_manifest_json(&manifest);
3886 let manifest_value: serde_json::Value =
3887 serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
3888
3889 assert_eq!(
3890 manifest_value["components"][0]["actions"][0]["operation"],
3891 serde_json::json!("add_assign")
3892 );
3893 assert_eq!(
3894 manifest_value["components"][0]["actions"][0]["operand"],
3895 serde_json::json!("2")
3896 );
3897 assert_eq!(
3898 manifest_value["components"][0]["actions"][1]["operation"],
3899 serde_json::json!("subtract_assign")
3900 );
3901 assert_eq!(
3902 manifest_value["components"][0]["actions"][1]["operand"],
3903 serde_json::json!("3")
3904 );
3905 }
3906
3907 #[test]
3908 fn builds_template_manifest_for_direct_assignment_action() {
3909 let source =
3910 include_str!("../../../fixtures/0011-direct-assignment/input/ResetCounter.tsx");
3911
3912 let parsed = presolve_parser::parse_file(
3913 "fixtures/0011-direct-assignment/input/ResetCounter.tsx",
3914 source,
3915 );
3916
3917 let component_graph = build_component_graph(&parsed);
3918 let template_graph = build_template_graph(&component_graph);
3919 let manifest = build_template_manifest(&component_graph, &template_graph);
3920
3921 assert_eq!(
3922 manifest.components[0].actions,
3923 vec![ManifestAction {
3924 method: "reset".to_string(),
3925 method_id: None,
3926 action_batch_id: None,
3927 operation: ManifestOperation::Assign,
3928 field: "count".to_string(),
3929 storage_id: None,
3930 operand: Some(SerializableValue::Number("0".to_string())),
3931 }]
3932 );
3933
3934 let manifest_json = template_manifest_json(&manifest);
3935 let manifest_value: serde_json::Value =
3936 serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
3937
3938 assert_eq!(
3939 manifest_value["components"][0]["actions"][0]["operation"],
3940 serde_json::json!("assign")
3941 );
3942 assert_eq!(
3943 manifest_value["components"][0]["actions"][0]["operand"],
3944 serde_json::json!("0")
3945 );
3946 }
3947
3948 #[test]
3949 fn builds_template_manifest_for_boolean_toggle_action() {
3950 let source = include_str!("../../../fixtures/0012-boolean-toggle/input/ToggleFlag.tsx");
3951
3952 let parsed = presolve_parser::parse_file(
3953 "fixtures/0012-boolean-toggle/input/ToggleFlag.tsx",
3954 source,
3955 );
3956
3957 let component_graph = build_component_graph(&parsed);
3958 let template_graph = build_template_graph(&component_graph);
3959 let manifest = build_template_manifest(&component_graph, &template_graph);
3960
3961 assert_eq!(
3962 manifest.components[0].actions,
3963 vec![ManifestAction {
3964 method: "toggle".to_string(),
3965 method_id: None,
3966 action_batch_id: None,
3967 operation: ManifestOperation::Toggle,
3968 field: "enabled".to_string(),
3969 storage_id: None,
3970 operand: None,
3971 }]
3972 );
3973
3974 let manifest_json = template_manifest_json(&manifest);
3975 let manifest_value: serde_json::Value =
3976 serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
3977
3978 assert_eq!(
3979 manifest_value["components"][0]["actions"][0]["operation"],
3980 serde_json::json!("toggle")
3981 );
3982 assert!(manifest_value["components"][0]["actions"][0]
3983 .get("operand")
3984 .is_none());
3985 }
3986
3987 #[test]
3988 fn builds_template_manifest_for_multi_step_action_in_source_order() {
3989 let source =
3990 include_str!("../../../fixtures/0013-multi-step-action/input/BatchActionCounter.tsx");
3991
3992 let parsed = presolve_parser::parse_file(
3993 "fixtures/0013-multi-step-action/input/BatchActionCounter.tsx",
3994 source,
3995 );
3996
3997 let component_graph = build_component_graph(&parsed);
3998 let template_graph = build_template_graph(&component_graph);
3999 let manifest = build_template_manifest(&component_graph, &template_graph);
4000
4001 assert_eq!(
4002 manifest.components[0].actions,
4003 vec![
4004 ManifestAction {
4005 method: "apply".to_string(),
4006 method_id: None,
4007 action_batch_id: None,
4008 operation: ManifestOperation::AddAssign,
4009 field: "count".to_string(),
4010 storage_id: None,
4011 operand: Some(SerializableValue::Number("2".to_string())),
4012 },
4013 ManifestAction {
4014 method: "apply".to_string(),
4015 method_id: None,
4016 action_batch_id: None,
4017 operation: ManifestOperation::Decrement,
4018 field: "count".to_string(),
4019 storage_id: None,
4020 operand: None,
4021 },
4022 ManifestAction {
4023 method: "apply".to_string(),
4024 method_id: None,
4025 action_batch_id: None,
4026 operation: ManifestOperation::Assign,
4027 field: "count".to_string(),
4028 storage_id: None,
4029 operand: Some(SerializableValue::Number("8".to_string())),
4030 },
4031 ManifestAction {
4032 method: "apply".to_string(),
4033 method_id: None,
4034 action_batch_id: None,
4035 operation: ManifestOperation::Increment,
4036 field: "count".to_string(),
4037 storage_id: None,
4038 operand: None,
4039 },
4040 ManifestAction {
4041 method: "apply".to_string(),
4042 method_id: None,
4043 action_batch_id: None,
4044 operation: ManifestOperation::Toggle,
4045 field: "enabled".to_string(),
4046 storage_id: None,
4047 operand: None,
4048 }
4049 ]
4050 );
4051
4052 let manifest_json = template_manifest_json(&manifest);
4053 let manifest_value: serde_json::Value =
4054 serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
4055
4056 let actions = manifest_value["components"][0]["actions"]
4057 .as_array()
4058 .expect("manifest actions should be an array");
4059
4060 assert_eq!(
4061 actions
4062 .iter()
4063 .map(|action| action["operation"].as_str().expect("operation is a string"))
4064 .collect::<Vec<_>>(),
4065 vec!["add_assign", "decrement", "assign", "increment", "toggle"]
4066 );
4067 assert_eq!(actions[0]["method"], serde_json::json!("apply"));
4068 assert_eq!(actions[4]["method"], serde_json::json!("apply"));
4069 assert!(actions[1].get("operand").is_none());
4070 assert!(actions[3].get("operand").is_none());
4071 assert!(actions[4].get("operand").is_none());
4072 }
4073
4074 #[test]
4075 fn builds_template_manifest_for_dynamic_attribute_bindings() {
4076 let source = include_str!(
4077 "../../../fixtures/0015-dynamic-attributes/input/DynamicAttributeButton.tsx"
4078 );
4079
4080 let parsed = presolve_parser::parse_file(
4081 "fixtures/0015-dynamic-attributes/input/DynamicAttributeButton.tsx",
4082 source,
4083 );
4084
4085 let component_graph = build_component_graph(&parsed);
4086 let template_graph = build_template_graph(&component_graph);
4087 let manifest = build_template_manifest(&component_graph, &template_graph);
4088 let manifest_json = template_manifest_json(&manifest);
4089 let manifest_value: serde_json::Value =
4090 serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
4091
4092 assert_eq!(
4093 manifest.components[0].template.nodes[1],
4094 ManifestNode::Binding {
4095 id: "n1".to_string(),
4096 expression: "this.disabled".to_string(),
4097 initial_value: Some(SerializableValue::Boolean(false)),
4098 target: Some(ManifestBindingTarget::Attribute),
4099 element: Some("n0".to_string()),
4100 attribute: Some("disabled".to_string()),
4101 }
4102 );
4103 assert_eq!(
4104 manifest_value["components"][0]["template"]["nodes"][1]["target"],
4105 serde_json::json!("attribute")
4106 );
4107 assert_eq!(
4108 manifest_value["components"][0]["template"]["nodes"][1]["element"],
4109 serde_json::json!("n0")
4110 );
4111 assert_eq!(
4112 manifest_value["components"][0]["template"]["nodes"][1]["attribute"],
4113 serde_json::json!("disabled")
4114 );
4115 assert!(manifest_value["components"][0]["template"]["nodes"][3]
4116 .get("target")
4117 .is_none());
4118 }
4119
4120 #[test]
4121 fn generates_standalone_page_with_embedded_manifest() {
4122 let source = include_str!("../../../fixtures/0004-nested-jsx/input/NestedCounter.tsx");
4123
4124 let parsed =
4125 presolve_parser::parse_file("fixtures/0004-nested-jsx/input/NestedCounter.tsx", source);
4126
4127 let component_graph = build_component_graph(&parsed);
4128 let template_graph = build_template_graph(&component_graph);
4129 let html = generate_static_html(&template_graph);
4130 let manifest = build_template_manifest(&component_graph, &template_graph);
4131 let page = generate_standalone_page("NestedCounter", &html, &manifest);
4132
4133 assert!(page.starts_with("<!doctype html>\n"));
4134 assert!(page.contains("<title>NestedCounter</title>"));
4135 assert!(page.contains("<section data-presolve-node=\"n0\">"));
4136 assert!(page.contains("id=\"presolve-template-manifest\""));
4137 assert!(page.contains("\"name\": \"NestedCounter\""));
4138 assert!(page.contains("<script src=\"./runtime.js\" defer></script>"));
4139 }
4140}