TypeScript proxy options compatibility matrix
Pinned reference versions
Section titled “Pinned reference versions”| Side | Component | Pinned version | Source |
|---|---|---|---|
| JVM | @cratis/arc and @cratis/arc.react | 22.10.4 | ContractTests/TypeScript/package.json |
| JVM | @cratis/fundamentals | 7.18.4 | ContractTests/TypeScript/package.json |
| .NET | Cratis.Arc.ProxyGenerator | 22.14.0 | parity.md capture harness; GradlePlugin/src/test/resources/differential/capture/capture.sh |
| .NET | .NET SDK | 10.0.400 | Same capture harness |
| .NET reference path | Source/DotNET/Tools/ProxyGenerator/ | Cratis/Arc read-only local checkout | Program.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.
Status key
Section titled “Status key”| Status | Meaning |
|---|---|
| Supported today | Present and working in the current JVM generator. |
| JVM-specific | Supported with a design that differs intentionally from .NET. |
| Deliberately unsupported | No equivalent; reason and the enforcing guard are named. Do not remove these guards. |
| Implementable with named slice | Not present; a named slice below defines exactly what to build, what would prove it, and what it must not break. |
segmentsToSkip: file layout versus route
Section titled “segmentsToSkip: file layout versus route”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:
| Setting | CLI option | Controls | Forwarded to |
|---|---|---|---|
endpoints.segmentsToSkip | --route-segments-to-skip | Leading package segments dropped from routes | ApiEndpointOptions.segmentsToSkipForRoute → EndpointRouteHelper |
proxies.segmentsToSkip | --proxy-segments-to-skip | Leading package segments dropped from generated file paths | ProxyGenerationOptions.segmentsToSkip → TypeScriptProxyGenerator.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 }}Option compatibility matrix
Section titled “Option compatibility matrix”The Arc .NET Program.cs usage line reads verbatim:
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 option | JVM option / DSL | Status | Notes |
|---|---|---|---|
segments-to-skip (positional) | endpoints.segmentsToSkip + proxies.segmentsToSkip | JVM-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-prefix | Supported today | Both default to api. |
--skip-command-name-in-route | endpoints.includeCommandNames = false | Supported today | Polarity is inverted; semantics are identical. |
--skip-query-name-in-route | endpoints.includeQueryNames = false | Supported today | Polarity is inverted; semantics are identical. |
--skip-output-deletion | proxies.removeStaleGeneratedFiles = false | Supported today | Polarity is inverted; semantics are identical. |
--type-to-ts=<Type>=<TsType>[=<Package>] | proxies.mapType(...) / --type-to-typescript | Supported today | Identical 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-npm | JVM-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> | None | Deliberately unsupported | The 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> | None | Deliberately unsupported | Same 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> | None | Implementable with named slice proxy-namespace-roots | See the named-slice section below. |
--skip-index-generation | None | Deliberately unsupported | TypeScriptProxyGenerator.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-mode | None | Deliberately unsupported | Arc .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-interfaces | None | Deliberately unsupported | The 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-file | None | Deliberately unsupported | The 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. |
Model-shape compatibility matrix
Section titled “Model-shape compatibility matrix”| Shape | Status | Current JVM contract and enforcing guard |
|---|---|---|
Class-hierarchy base type (extends) | Supported today | TypeDescriptor.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 class | Deliberately unsupported | resolveBaseType() 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 derivative | Deliberately unsupported | resolveDerivatives() 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 values | Supported today | resolveMapShape() 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 keys | Deliberately unsupported | resolveMapShape() 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 values | Deliberately unsupported | resolveMapShape() 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 paths | Deliberately unsupported | resolveMapShape() 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 today | TypeShapeKind.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 context | Deliberately unsupported | appendSharedEdge() throws GradleException("[ARCVALIDATION_GRAPH] Shared model maps are unsupported."). Client-side shared validation does not traverse map entries. |
| Interface declared properties | Supported today | InterfaceDescriptor.properties generates a TypeScript export interface block via renderInterface(). Properties are emitted in declaration order with optional markers. |
Interface inheritance (extends between interfaces) | Deliberately unsupported | InterfaceDescriptor 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 today | returnedSortProperties() 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 boundary | When 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 unsupported | When 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 KSP | Supported today | ArcManifestDiscovery.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 KSP | Deliberately unsupported | Libraries 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. |
Named slice: proxy-namespace-roots
Section titled “Named slice: proxy-namespace-roots”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.
- Add
namespaceRoots: List<Pair<String, String>>toProxyGenerationOptionsand a correspondingproxies.mapNamespaceRoot(jvmPackage: String, outputFolder: String)method onArcProxyOptions. - Update
outputDirectory(location: List<String>)inTypeScriptProxyGenerator.ktto: join thelocationlist 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 fromoutputFolder + remainder, and fall back to the existingsegmentsToSkipdrop when no root matches. - Add
--namespace-rootas a repeatable<jvmPackage=outputFolder>option toGenerateArcProxiesCli.kt. - 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:verifyContractTestProxyDeterminismstays green (root matching must produce a consistent sort order across consecutive runs).- No dangling imports, path escapes, or
index.tscollisions in the output tree.
What it must not break.
- The
segmentsToSkipfallback for types outside every configured root. - The
validateTarget()path-safety and escape guards. - The
sanitizeArcStringMapguard and all existing map-property protections. - The proxy differential gate (
:GradlePlugin:test) and strict TypeScript compile gate (:ContractTests:typeScriptBuild).
Relationship to parity.md
Section titled “Relationship to parity.md”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.