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