-
Notifications
You must be signed in to change notification settings - Fork 1
ExternalTypeResolution
The eQuantic.UI compiler can now resolve types defined in external files within the same project. This enables components to reference models, DTOs, and other classes without requiring them to be in the same file.
Previously, the compiler used a minimal SemanticModel that only included the component file being compiled. This meant:
❌ Before: Cannot reference external classes
// Models/User.cs
public class User
{
public string Name { get; set; }
public string Email { get; set; }
}
// Pages/UserProfile.cs
[Page("/profile")]
public class UserProfile : StatefulComponent
{
private User _currentUser; // ❌ Type 'User' not resolved - generates incorrect JavaScript
protected override HtmlNode Render()
{
return Text(_currentUser.Name); // ❌ _currentUser.Name conversion fails
}
}The compiler now supports passing the full project Roslyn Compilation, which includes all source files and references.
✅ After: External classes work perfectly
// Models/User.cs
public class User
{
public string Name { get; set; }
public string Email { get; set; }
}
// Pages/UserProfile.cs
[Page("/profile")]
public class UserProfile : StatefulComponent
{
private User _currentUser; // ✅ Type 'User' fully resolved
protected override HtmlNode Render()
{
return Text(_currentUser.Name); // ✅ Converts to: this._currentUser.name
}
}using eQuantic.UI.Compiler;
using eQuantic.UI.Compiler.Services;
// Get the full project compilation
var compilation = await ProjectCompilationHelper
.GetProjectCompilationAsync("path/to/MyApp.csproj");
// Create compiler and set project compilation
var compiler = new ComponentCompiler();
compiler.SetProjectCompilation(compilation);
// Now compile components - external types will be resolved
var results = compiler.CompileFile("Pages/UserProfile.cs");Useful in MSBuild tasks where MSBuildWorkspace might not be available:
using eQuantic.UI.Compiler.Services;
// Get all .cs files in the project
var sourceFiles = ProjectCompilationHelper
.GetProjectSourceFiles("path/to/MyApp");
// Get assembly references
var assemblyPaths = new[]
{
"path/to/eQuantic.UI.Core.dll",
"path/to/other-dependencies.dll"
};
// Create compilation from sources
var compilation = ProjectCompilationHelper.CreateCompilationFromSources(
sourceFiles,
assemblyPaths,
"MyApp");
var compiler = new ComponentCompiler();
compiler.SetProjectCompilation(compilation);Revert to minimal compilation mode (isolated files):
compiler.ClearProjectCompilation();┌─────────────────────────────────────────┐
│ MSBuild Project Compilation │
│ - All .cs files in project │
│ - All referenced assemblies │
│ - Full type information │
└────────────┬────────────────────────────┘
│
↓ SetProjectCompilation()
┌────────────────────────────────────────┐
│ SemanticModelProvider │
│ - Stores project compilation │
│ - Returns SemanticModel for each file │
└────────────┬───────────────────────────┘
│
↓ GetSemanticModel(tree)
┌────────────────────────────────────────┐
│ CSharpToJsConverter │
│ - Uses SemanticModel for type info │
│ - Converts _currentUser.Name → name │
└────────────────────────────────────────┘
-
SemanticModelProvider(SemanticModelProvider.cs)-
SetProjectCompilation(Compilation)- Set full project compilation -
GetSemanticModel(SyntaxTree)- Returns semantic model with full type info
-
-
ProjectCompilationHelper(ProjectCompilationHelper.cs)-
GetProjectCompilationAsync(string)- Load from .csproj -
CreateCompilationFromSources(...)- Build from source files -
GetProjectSourceFiles(string)- Find all .cs files
-
-
ComponentCompiler(ComponentCompiler.cs)-
SetProjectCompilation(Compilation)- Enable external type resolution -
ClearProjectCompilation()- Revert to isolated mode
-
Models/Product.cs:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public bool InStock { get; set; }
}Pages/ProductCard.cs:
[Component]
public class ProductCard : StatelessComponent
{
public Product Item { get; set; }
protected override HtmlNode Build()
{
return Container(
Heading(Item.Name),
Text($"${Item.Price:F2}"),
Text(Item.InStock ? "In Stock" : "Out of Stock")
);
}
}Generated JavaScript (with project compilation):
class ProductCard extends StatelessComponent {
build() {
return Container([
Heading(this.item.name),
Text(`$${this.item.price.toFixed(2)}`),
Text(this.item.inStock ? 'In Stock' : 'Out of Stock')
]);
}
}Models/Address.cs:
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
}Models/Customer.cs:
public class Customer
{
public string Name { get; set; }
public string Email { get; set; }
public Address ShippingAddress { get; set; }
}Pages/CheckoutPage.cs:
[Page("/checkout")]
public class CheckoutPage : StatefulComponent
{
private Customer _customer;
protected override HtmlNode Render()
{
return Container(
Text($"Customer: {_customer.Name}"),
Text($"Email: {_customer.Email}"),
Text($"Shipping: {_customer.ShippingAddress.City}, {_customer.ShippingAddress.ZipCode}")
);
}
}All property accesses are correctly resolved and converted to JavaScript.
Tests verify external type resolution works correctly:
[Fact]
public void SemanticModel_WithProjectCompilation_CanResolveExternalTypes()
{
// Create compilation with User and Component
var userTree = CSharpSyntaxTree.ParseText("public class User { ... }");
var componentTree = CSharpSyntaxTree.ParseText("public class UserProfile { ... }");
var compilation = CSharpCompilation.Create("Test", new[] { userTree, componentTree }, ...);
var provider = new SemanticModelProvider();
provider.SetProjectCompilation(compilation);
var semanticModel = provider.GetSemanticModel(componentTree);
// Assert: User type is resolved
var userType = semanticModel.Compilation.GetTypeByMetadataName("User");
userType.Should().NotBeNull();
}See ExternalTypeResolutionTests.cs for complete test suite.
The feature is fully backwards compatible:
- Without
SetProjectCompilation(): Works as before (isolated files only) - With
SetProjectCompilation(): Gains full project type resolution
No breaking changes to existing code.
Minimal overhead:
- Project compilation is created once at build time
- Shared across all component files
- No per-file compilation penalty
Memory efficient:
- Single compilation instance
- Reuses existing Roslyn infrastructure
-
Requires compilation before component compilation: The project must be compiled (or at least parsed) before running the component compiler.
-
MSBuildWorkspace availability:
GetProjectCompilationAsyncrequires MSBuild APIs, which may not be available in all contexts. UseCreateCompilationFromSourcesas fallback. -
Generated code: Auto-generated files in
obj/are excluded to avoid conflicts.
Potential improvements:
- Automatic detection: Auto-discover .csproj from component file location
- Caching: Cache compilations for faster repeated builds
- Incremental: Support incremental compilation for changed files only
- Diagnostics: Report type resolution failures with actionable errors
✅ What's New:
- Components can reference classes from other files
- Full type information available during compilation
- Correct JavaScript generation for external type members
✅ Benefits:
- Better code organization (models in separate files)
- Type-safe property access
- More accurate transpilation
✅ How to Use:
var compilation = await ProjectCompilationHelper.GetProjectCompilationAsync(projectPath);
compiler.SetProjectCompilation(compilation);✅ Tests: 294 tests passing (5 new external type tests)