Let a consumer declare how a type crosses the wire - #2442
Conversation
The generator's type map is a static with a collection initializer: no setter, no add, no fallback. A consumer with a domain type that should reach TypeScript as something other than what the map decides has nowhere to say so, and neither does one who needs an existing mapping corrected - both need a release of the generator. Add a repeatable --type-to-ts argument, fed from a TypeToTsType MSBuild item, following the pattern AssemblyToPackageMapping already established. It is consulted ahead of the built-in map, which is what lets it correct an existing mapping rather than only add an unknown one; consulted after, it could never reach a type the generator already knows. Deliberately not an attribute. A build-time concern belongs in the build file rather than on the domain type, and an attribute cannot express a mapping for a type the consumer does not own. Generated proxies are committed in consumer repositories, so the guarantee worth protecting is that a build configuring nothing generates exactly what it generated before: a spec covers configure-then-clear leaving no residue, and the 1055 existing generator specs still pass untouched.
|
Reviewer context — verification and design notes, kept out of the release notes. Design
Verification
Notes
|
There was a problem hiding this comment.
Pull request overview
Adds an opt-in mechanism for consumers to override the .NET→TypeScript “cross-the-wire” type mapping used by the ProxyGenerator, and adjusts @cratis/arc’s NPM dependency model to treat @cratis/fundamentals as a peer dependency.
Changes:
- Added repeatable
--type-to-ts/TypeToTsTypeplumbing (MSBuild → CLI → generator) and applied it ahead of the built-in type map. - Introduced specs covering configured/override/cleared type mappings behavior.
- Changed
@cratis/arcto move@cratis/fundamentalsfromdependenciestopeerDependencies(with a pinneddevDependencyfor local development).
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| Source/JavaScript/Arc/package.json | Moves @cratis/fundamentals to peerDependencies and pins it as a dev dependency for development. |
| Source/DotNET/Tools/ProxyGenerator/TypeExtensions.cs | Adds configurable per-type TS mappings and consults them before the built-in map. |
| Source/DotNET/Tools/ProxyGenerator/Program.cs | Parses repeatable --type-to-ts args and passes mappings into generation. |
| Source/DotNET/Tools/ProxyGenerator/Generator.cs | Wires typeMappings into TypeExtensions initialization. |
| Source/DotNET/Tools/ProxyGenerator.Specs/for_TypeExtensions/when_no_type_mappings_are_configured.cs | Verifies clearing mappings leaves prior behavior unchanged. |
| Source/DotNET/Tools/ProxyGenerator.Specs/for_TypeExtensions/when_a_type_mapping_overrides_a_built_in_one.cs | Verifies configured mappings override built-in ones. |
| Source/DotNET/Tools/ProxyGenerator.Specs/for_TypeExtensions/when_a_type_mapping_is_configured.cs | Verifies configured mappings produce expected TS type and import metadata. |
| Source/DotNET/Tools/ProxyGenerator.Specs/for_TypeExtensions/when_a_type_mapping_is_configured_without_a_package.cs | Verifies mapping without package produces no import. |
| Source/DotNET/Tools/ProxyGenerator.Specs/for_TypeExtensions/given/no_type_mappings.cs | Adds shared fixture ensuring mappings are cleared between specs. |
| Source/DotNET/Tools/ProxyGenerator.Build/build/Cratis.Arc.ProxyGenerator.Build.targets | Adds MSBuild item transform to emit --type-to-ts args to the generator. |
| public static void SetTypeMappings(IReadOnlyCollection<(string TypeName, string TsType, string Package)> mappings) | ||
| { | ||
| _typeMappings = mappings.ToDictionary( | ||
| static mapping => mapping.TypeName, | ||
| static mapping => new TargetType( | ||
| typeof(object), | ||
| mapping.TsType, | ||
| mapping.TsType, | ||
| mapping.Package, | ||
| Final: true, | ||
| FromPackage: !string.IsNullOrEmpty(mapping.Package)), | ||
| StringComparer.Ordinal); | ||
| } |
| [Fact] void should_generate_the_declared_type() => _result.Type.ShouldEqual("string"); | ||
| [Fact] void should_not_import_anything() => _result.Module.ShouldBeEmpty(); | ||
| [Fact] void should_not_be_from_a_package() => _result.FromPackage.ShouldBeFalse(); | ||
| } |
8551674 to
b89e9ef
Compare
|
Relabelled minor and the peer-dependency change moved out to #2445. Bundling them was wrong: this half is purely additive and opt-in — a build passing no |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (3)
Source/DotNET/Tools/ProxyGenerator/TypeExtensions.cs:136
SetTypeMappings()usesToDictionary(), which throws if the same type name is configured more than once (e.g., duplicated MSBuild items). That would crash proxy generation with a generic exception. Consider making mappings deterministic (e.g., last-one-wins) to avoid fragile builds and allow intentional overrides.
public static void SetTypeMappings(IReadOnlyCollection<(string TypeName, string TsType, string Package)> mappings)
{
_typeMappings = mappings.ToDictionary(
static mapping => mapping.TypeName,
static mapping => new TargetType(
typeof(object),
mapping.TsType,
mapping.TsType,
mapping.Package,
Final: true,
FromPackage: !string.IsNullOrEmpty(mapping.Package)),
StringComparer.Ordinal);
}
Source/DotNET/Tools/ProxyGenerator/Program.cs:58
- Parsing
--type-to-tsusesentry.Split('='), which will split on every '=' and can mis-handle inputs if the TS type or package ever contains '='. Limiting the split to 3 parts avoids accidental truncation while still supporting the intended<Type>=<TsType>[=<Package>]format.
foreach (var entry in args.Where(_ => _.StartsWith("--type-to-ts=")).Select(_ => _["--type-to-ts=".Length..]))
{
var parts = entry.Split('=');
if (parts.Length >= 2 && !string.IsNullOrWhiteSpace(parts[0]) && !string.IsNullOrWhiteSpace(parts[1]))
{
typeMappings.Add((parts[0], parts[1], parts.Length > 2 ? parts[2] : string.Empty));
Source/DotNET/Tools/ProxyGenerator.Build/build/Cratis.Arc.ProxyGenerator.Build.targets:44
TypeToTsTypeMSBuild items are expanded into the Exec command without quoting. If%(TsType)ever contains spaces (e.g., a union type likeFoo | Bar), the command line will split it into multiple args and the mapping will be parsed incorrectly. Quoting each--type-to-ts=...token avoids this class of failures.
<ExcludeTypes>@(ExcludeType -> '--exclude-type=%(TypeName)', ' ')</ExcludeTypes>
<ExcludeNamespaces>@(ExcludeNamespace -> '--exclude-namespace=%(Namespace)', ' ')</ExcludeNamespaces>
<NamespaceRoots>@(NamespaceRoot -> '--namespace-root=%(Namespace)=%(Folder)', ' ')</NamespaceRoots>
<TypeMappings>@(TypeToTsType -> '--type-to-ts=%(TypeName)=%(TsType)=%(Package)', ' ')</TypeMappings>
</PropertyGroup>
<Exec ConsoleToMsBuild="true"
Command="dotnet $(CratisProxyGeneratorAssembly) "$(MSBuildProjectDirectory)/$(OutputPath)$(AssemblyName).dll" "$(CratisProxiesOutputPath)" $(CratisProxiesSegmentsToSkip) $(LibraryMode) $(SkipOutputDeletion) $(SkipCommandNameInRoute) $(SkipQueryNameInRoute) $(ApiPrefix) --project-directory="$(MSBuildProjectDirectory)" $(SkipFileIndexTracking) $(SkipIndexGeneration) $(UseSourceFileAsOutputFile) $(AssemblyPackageMappings) $(ExcludeTypes) $(ExcludeNamespaces) $(NamespaceRoots) $(TypeMappings)"
Declared as a range in dependencies, which tells a package manager a copy each is acceptable. It is not: the two copies get separate converter registries and separate class objects, so a converter registered on one reaches only that one, an instanceof against the other is false, and a version pinned at the top level never reaches the nested copy - an adopted fix can land nowhere while every build stays green. A peer dependency is the mechanism for "exactly one of these in the tree", which is what this actually is. The range is deliberately wide so a patch or minor release does not force lockstep releases across repositories, and an exact devDependency keeps this repository's own build and specs pinned.
|
Recombined with the peer-dependency change and relabelled major; #2445 is closed as redundant.
The two changes remain separate commits, so they stay independently reviewable and revertable. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
Source/DotNET/Tools/ProxyGenerator/TypeExtensions.cs:130
- SetTypeMappings() uses ToDictionary(), which throws if the same TypeName appears more than once. Since mappings come from repeatable MSBuild items/args, duplicates are a realistic user-input case and would currently crash proxy generation instead of resolving deterministically.
_typeMappings = mappings.ToDictionary(
static mapping => mapping.TypeName,
static mapping => new TargetType(
typeof(object),
mapping.TsType,
| Console.WriteLine(" Cratis.ProxyGenerator <assembly> <output-path> [segments-to-skip] [--library-mode] [--skip-output-deletion] [--skip-command-name-in-route] [--skip-query-name-in-route] [--api-prefix=<prefix>] [--skip-index-generation] [--use-source-file-as-output-file] [--assembly-to-package=<Assembly>=<Package>]... [--exclude-type=<FullyQualifiedTypeName>]... [--exclude-namespace=<Pattern>]... [--namespace-root=<Namespace>=<Folder>]... [--type-to-ts=<FullyQualifiedTypeName>=<TsType>[=<Package>]]..."); | ||
| return 1; | ||
| } | ||
| var assemblyFile = Normalize(Path.GetFullPath(args[0])); |
| // Ahead of the built-in map, which is what lets a mapping correct an existing type rather than | ||
| // only add an unknown one. The built-in map is still what answers when nothing was configured. | ||
| if (type.FullName is not null && _typeMappings.TryGetValue(type.FullName, out var mapped)) | ||
| { | ||
| return mapped with { OriginalType = type }; |
|
Superseded by #2446, which carries this unchanged so the whole set lands as one release rather than four. Closing so it cannot be merged twice — the branch is untouched. |
|
Noting for whoever picks this work up, since this PR was closed unmerged and the branch still exists. CI on this PR failed one job — Fixed in
|
Added
--type-to-tsargument and matchingTypeToTsTypeMSBuild item declare how a .NET type crosses the wire to TypeScript, and take the place of the built-in mapping for that type.Changed
@cratis/arcdeclares@cratis/fundamentalsas a peer dependency instead of a versioned dependency. Consumers whose package manager does not install peers automatically must now declare it themselves.