Skip to content

TypeScript proxy options compatibility matrix

SideComponentPinned versionSource
JVM@cratis/arc and @cratis/arc.react22.10.4ContractTests/TypeScript/package.json
JVM@cratis/fundamentals7.18.4ContractTests/TypeScript/package.json
.NETCratis.Arc.ProxyGenerator22.14.0parity.md capture harness; GradlePlugin/src/test/resources/differential/capture/capture.sh
.NET.NET SDK10.0.400Same capture harness
.NET reference pathSource/DotNET/Tools/ProxyGenerator/Cratis/Arc read-only local checkoutProgram.cs, TypeExtensions.cs, Generator.cs read directly

Every row below was verified against code read at these versions. The .NET Program.cs usage string is quoted in the option table; TypeExtensions.IsExcluded, SetNamespaceRoots, and ResolveTargetPath were read from TypeExtensions.cs; and the JVM guards were read from TypeScriptProxyGenerator.kt in this repository. No source was assumed to match without reading it.

StatusMeaning
Supported todayPresent and working in the current JVM generator.
JVM-specificSupported with a design that differs intentionally from .NET.
Deliberately unsupportedNo equivalent; reason and the enforcing guard are named. Do not remove these guards.
Implementable with named sliceNot present; a named slice below defines exactly what to build, what would prove it, and what it must not break.

Arc .NET exposes one positional segments-to-skip argument that drops the same number of leading namespace segments from both the generated file path and the computed route. The JVM splits this into two independent settings with separate CLI options:

SettingCLI optionControlsForwarded to
endpoints.segmentsToSkip--route-segments-to-skipLeading package segments dropped from routesApiEndpointOptions.segmentsToSkipForRouteEndpointRouteHelper
proxies.segmentsToSkip--proxy-segments-to-skipLeading package segments dropped from generated file pathsProxyGenerationOptions.segmentsToSkipTypeScriptProxyGenerator.outputDirectory()

Setting one and expecting the other to follow will not work: they are independent. Both default to 0. The contract tests fix both at 5 via the proxyArguments() block in GradlePlugin/build.gradle.kts. The --namespace-root slice described later provides a named-root alternative to segmentsToSkip for file layout.

A project that wants routes and file paths to diverge — for example, a route at /api/features/orders but a proxy at orders/Orders.ts — uses a different value for each setting. This split has no .NET equivalent.

cratisArc {
endpoints {
segmentsToSkip = 3 // drops 3 leading segments from generated ROUTES
}
proxies {
segmentsToSkip = 3 // drops 3 leading segments from generated FILE PATHS
}
}

The Arc .NET Program.cs usage line reads verbatim:

Terminal window
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]
[--emit-interfaces] [--assembly-to-package=<Assembly>=<Package>]...
[--exclude-type=<FullyQualifiedTypeName>]... [--exclude-namespace=<Pattern>]...
[--namespace-root=<Namespace>=<Folder>]... [--type-to-ts=<FullyQualifiedTypeName>=<TsType>[=<Package>]]...
.NET optionJVM option / DSLStatusNotes
segments-to-skip (positional)endpoints.segmentsToSkip + proxies.segmentsToSkipJVM-specific.NET applies one value to both route and file path. The JVM requires two separate values. See the previous section.
--api-prefix=<prefix>endpoints.routePrefix / --route-prefixSupported todayBoth default to api.
--skip-command-name-in-routeendpoints.includeCommandNames = falseSupported todayPolarity is inverted; semantics are identical.
--skip-query-name-in-routeendpoints.includeQueryNames = falseSupported todayPolarity is inverted; semantics are identical.
--skip-output-deletionproxies.removeStaleGeneratedFiles = falseSupported todayPolarity is inverted; semantics are identical.
--type-to-ts=<Type>=<TsType>[=<Package>]proxies.mapType(...) / --type-to-typescriptSupported todayIdentical semantics: consulted ahead of the built-in type map; bounded three-part = split; warns and skips unusable entries. ProxyTypeMappings.parseTypeMappings() mirrors the .NET bounded-split comment exactly.
--assembly-to-package=<Assembly>=<Package>proxies.mapPackage(jvmPackage, npmPackage) / --package-to-npmJVM-specific.NET keys on the assembly name (e.g., MyLib). The JVM has no assembly concept: it keys on a fully qualified JVM package prefix (e.g., com.example.shared), and the longest matching prefix wins. parity.md records this divergence in the “External TypeScript package mappings and type overrides” row.
--exclude-type=<FullyQualifiedTypeName>NoneDeliberately unsupportedThe JVM generates only types that appear in the KSP-produced manifest. A manifest type referenced from a command, query, or model cannot be excluded without also providing a TypeScript replacement: resolveType() in TypeScriptProxyGenerator.kt throws GradleException("Unsupported Arc proxy type '...' in '...'.") for any unresolvable reference. Use mapType() to redirect a type to an external npm import instead.
--exclude-namespace=<Pattern>NoneDeliberately unsupportedSame reasoning as --exclude-type. Use mapPackage() to redirect an entire JVM package to an external npm package. The same resolveType() guard applies for unresolvable types.
--namespace-root=<Namespace>=<Folder>NoneImplementable with named slice proxy-namespace-rootsSee the named-slice section below.
--skip-index-generationNoneDeliberately unsupportedTypeScriptProxyGenerator.updateIndexFiles() is called unconditionally from generate(). Barrel index files are part of the consumer-facing import contract; suppressing them breaks directory-level imports. There is no flag to skip this behavior and no plan to add one.
--library-modeNoneDeliberately unsupportedArc .NET emits proxies for every public type in the loaded assembly under --library-mode. The JVM model is annotation-driven: only types reachable from @Command, @ReadModel, and @ExportedType-annotated sources enter the KSP manifest. Bulk emission without annotation is outside the JVM model.
--emit-interfacesNoneDeliberately unsupportedThe JVM emits model classes with @field decorators from @cratis/fundamentals 7.18.4 because the client runtime uses them for JSON hydration. Arc .NET’s --emit-interfaces strips @field and the runtime dependency for packages that never deserialize. The JVM does not expose this mode because it would break the client hydration contract.
--use-source-file-as-output-fileNoneDeliberately unsupportedThe JVM emits one .ts file per type. Grouping by source file is a C# idiom (a single .cs file may define multiple types); the JVM has no analogous concept and no grouping mechanism.
ShapeStatusCurrent JVM contract and enforcing guard
Class-hierarchy base type (extends)Supported todayTypeDescriptor.baseTypeName carries the qualified base class name. renderType() emits export class Foo extends Base { … }. resolveBaseType() requires the resolved base to be an ArtifactKind.TYPE; if not, it throws GradleException("Arc base type '...' must be a generated model type.").
External (mapped) type as base classDeliberately unsupportedresolveBaseType() in TypeScriptProxyGenerator.kt throws for any resolved base that is not a generated ArtifactKind.TYPE. Emitting extends ExternalType would require @field constructor metadata the generator cannot produce for types it does not own. Do not remove this guard.
Polymorphic derivative hydration (manifest-registered types)Supported today@DerivedType writes a discriminator; KSP records the derivative mapping in the manifest; renderType() emits @field(Constructor, isEnumerable, [Derivatives…]); DerivedTypeRegistry handles runtime hydration. See parity.md “Polymorphic derived types” row for the JVM-specific DerivedTypeRegistrar requirement.
External (mapped) type as derivativeDeliberately unsupportedresolveDerivatives() in TypeScriptProxyGenerator.kt throws GradleException("Arc derivative '...' must be a generated concrete model type.") for any type that is not an ArtifactKind.TYPE. Do not remove this guard.
Map with nonnullable String keys and safe primitive scalar valuesSupported todayresolveMapShape() accepts MAP_STRING_TYPE_NAMES (kotlin.String, java.lang.String, String) keys and MAP_SAFE_PRIMITIVE_TYPE_NAMES value leaves (boolean, byte, char, int, short, String and their JVM/Kotlin variants). A runtime prototype-pollution guard is injected as sanitizeArcStringMap().
Map with non-String keysDeliberately unsupportedresolveMapShape() throws GradleException("Map property '...' entry path '...key' must use nonnullable String keys.") when keyCodec != MapKeyCodec.STRING or the key type is outside MAP_STRING_TYPE_NAMES. Arc .NET emits ValueMap<K,V> from @cratis/fundamentals for non-string-key maps; the pinned @cratis/fundamentals 7.18.4 client exports ValueMap, but the JVM manifest carries no non-string key codec so the generator cannot produce correct ValueMap output. Do not remove this guard.
Map with typed model or interface valuesDeliberately unsupportedresolveMapShape() throws GradleException("Map property '...' entry path '...' has unsupported value leaf '...'.") for value types outside MAP_SAFE_PRIMITIVE_TYPE_NAMES. Typed model values would require nested @field/hydration metadata on the map value path; the current map shape does not emit these.
Map with nullable entry pathsDeliberately unsupportedresolveMapShape() throws GradleException("Map property '...' entry path '...' cannot be nullable.") for any nullable path beyond the map root.
Sequence of sequences (nested arrays as map value)Supported todayTypeShapeKind.SEQUENCE nested inside another SEQUENCE in resolveMapShape() is handled: the element shape is resolved recursively and the TypeScript type becomes element[].
Map in a shared-validator contextDeliberately unsupportedappendSharedEdge() throws GradleException("[ARCVALIDATION_GRAPH] Shared model maps are unsupported."). Client-side shared validation does not traverse map entries.
Interface declared propertiesSupported todayInterfaceDescriptor.properties generates a TypeScript export interface block via renderInterface(). Properties are emitted in declaration order with optional markers.
Interface inheritance (extends between interfaces)Deliberately unsupportedInterfaceDescriptor carries no base-interface field. Manifest format 8 records no interface inheritance chain. Emitted TypeScript interfaces have no extends clause. Sort helpers for interface return types include only the interface’s own declared properties; returnedSortProperties() contains the comment // Interface inheritance is not represented by the manifest; do not invent missing members. A format bump and renderer change would both be required before this could be supported.
Result-field sort helpers (class return type, including class-base chain)Supported todayreturnedSortProperties() walks the TypeDescriptor.baseTypeName chain of the return type, collecting all declared properties at every level into a sorted set. ${ClassName}SortBy and ${ClassName}SortByWithoutQuery helpers are generated for every named field. Cyclic inheritance is detected and throws GradleException.
Result-field sort helpers (interface return type)Supported today, with boundaryWhen the return type resolves to an InterfaceDescriptor, returnedSortProperties() reads only its declared properties. It does not walk interface inheritance because the manifest records no inheritance chain. Sort keys reflect only the declared members of that specific interface.
Result-field sort helpers (external or dependency-only return type)Deliberately unsupportedWhen the return type resolves to an external (mapped) type, returnedSortProperties() finds no entry in artifacts.types or artifacts.interfaces and falls through without producing sort helpers. No exception is thrown; the supportsSorting capability flag remains authoritative, but the generated helper classes are empty.
Types from dependency libraries built with Arc KSPSupported todayArcManifestDiscovery.discover() scans META-INF/cratis/arc/*.json from every directory and jar on the configured manifest classpath, including dependency jars. Types from libraries that ran Arc KSP and produced a manifest are fully discoverable.
Types from dependency libraries not built with Arc KSPDeliberately unsupportedLibraries that were not built with Arc KSP produce no META-INF/cratis/arc/*.json manifest entry. ArcManifestDiscovery cannot discover their types and the generator cannot produce proxies for them. Use mapPackage() or mapType() to import such types from a published npm package. The arc.responseHandlerMetadata dependency handler index handles response-handler classification from binaries separately; it is not a model-type manifest.

Arc .NET’s --namespace-root=<Namespace>=<Folder> (implemented in TypeExtensions.ResolveTargetPath()) strips a namespace prefix from the output path and places the remainder under a named folder. When multiple roots are configured the longest matching prefix wins. This is a file-layout control only and does not affect routes.

What the JVM slice would do.

  1. Add namespaceRoots: List<Pair<String, String>> to ProxyGenerationOptions and a corresponding proxies.mapNamespaceRoot(jvmPackage: String, outputFolder: String) method on ArcProxyOptions.
  2. Update outputDirectory(location: List<String>) in TypeScriptProxyGenerator.kt to: join the location list back into a dot-separated package string, attempt to match the longest configured root whose value is a strict package-boundary prefix of that string, build the output path from outputFolder + remainder, and fall back to the existing segmentsToSkip drop when no root matches.
  3. Add --namespace-root as a repeatable <jvmPackage=outputFolder> option to GenerateArcProxiesCli.kt.
  4. Apply the existing validateTarget() safe-path and root-escape guards unchanged to every segment produced by the new logic; the path-escape check (!destination.startsWith(root)) is unconditional.

What would prove it.

  • At least two generator unit tests: one asserting that a type whose package matches a root lands under the mapped folder; one asserting that a type outside every root still uses segmentsToSkip.
  • A test asserting that a root whose folder component would produce an unsafe segment is rejected before output writes.
  • GradlePlugin:verifyContractTestProxyDeterminism stays green (root matching must produce a consistent sort order across consecutive runs).
  • No dangling imports, path escapes, or index.ts collisions in the output tree.

What it must not break.

  • The segmentsToSkip fallback for types outside every configured root.
  • The validateTarget() path-safety and escape guards.
  • The sanitizeArcStringMap guard and all existing map-property protections.
  • The proxy differential gate (:GradlePlugin:test) and strict TypeScript compile gate (:ContractTests:typeScriptBuild).

The “External TypeScript package mappings and type overrides” row in parity.md already records: “The JVM keys package mappings on a JVM package prefix because it has no assembly identity to key on; .NET’s --exclude-type, --exclude-namespace, and --namespace-root options have no equivalent and none is claimed.” This matrix extends that record with per-option rationale, named enforcing guards, and the proxy-namespace-roots slice definition.

No parity status changes follow from this document alone. A status upgrade requires a test, contract test, or runnable sample that proves the behavior, named in the same change. Do not interpret this matrix as a claim that all TypeScript mappings now match Arc .NET.