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