From c79daca34bbee9a6cb0623c4c5ac16f72aabd261 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:48:08 +0000 Subject: [PATCH 01/15] Initial plan From d6d7c2a270775c9a435c1062aa76a6991ec056e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:05:05 +0000 Subject: [PATCH 02/15] Expand back-compat property type support to all model properties Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/33d000a2-d811-4f6c-a72b-359b57cee1a2 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 107 +++++++++++++++--- .../ModelProviders/ModelProviderTests.cs | 86 ++++++++++++++ .../MockInputModel.cs | 11 ++ .../MockInputModel.cs | 7 ++ .../MockInputModel.cs | 7 ++ .../generator/docs/backward-compatibility.md | 39 ++++++- 6 files changed, 237 insertions(+), 20 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_EnumPropertyTypeIsRetainedWhenNullabilityDiffers/MockInputModel.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_NullableScalarPropertyTypeIsRetained/MockInputModel.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ScalarPropertyTypeNotOverriddenWhenTypeNameDiffers/MockInputModel.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index 83412b4f19d..72339f66319 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Microsoft.TypeSpec.Generator.Expressions; @@ -535,24 +536,13 @@ protected internal override PropertyProvider[] BuildProperties() continue; } - // Targeted backcompat fix for the case where properties were previously generated as read-only collections - if (outputProperty.Type.IsReadWriteList || outputProperty.Type.IsReadWriteDictionary) + // Backcompat fix for property types: if a property existed in the last contract + // with a compatible but non-identical type, retain the previous type to avoid + // breaking consumers of the library. + if (TryGetLastContractPropertyTypeOverride(outputProperty, out var lastContractPropertyType)) { - // We compare Arguments by name (not just ElementType) to cover both list element types - // and dictionary key/value types. This ensures we only override the collection wrapper - // (e.g. IReadOnlyList → IList) and not when the element type itself has changed. - // We use AreNamesEqual rather than Equals because the argument types may come from - // different sources (TypeProvider vs compiled assembly) but represent the same logical type. - if (LastContractPropertiesMap.TryGetValue(outputProperty.Name, - out CSharpType? lastContractPropertyType) && - !outputProperty.Type.Equals(lastContractPropertyType) && - outputProperty.Type.Arguments.Count == lastContractPropertyType.Arguments.Count && - outputProperty.Type.Arguments.Zip(lastContractPropertyType.Arguments).All( - pair => pair.First.AreNamesEqual(pair.Second))) - { - outputProperty.Type = lastContractPropertyType.ApplyInputSpecProperty(property); - CodeModelGenerator.Instance.Emitter.Info($"Changed property {Name}.{outputProperty.Name} type to {lastContractPropertyType} to match last contract."); - } + outputProperty.Type = lastContractPropertyType.ApplyInputSpecProperty(property); + CodeModelGenerator.Instance.Emitter.Info($"Changed property {Name}.{outputProperty.Name} type to {lastContractPropertyType} to match last contract."); } if (!isDiscriminator) @@ -1276,6 +1266,89 @@ _ when type.Equals(_additionalPropsUnknownType, ignoreNullable: true) => type, }; } + /// + /// Determines whether the type of a generated property should be replaced with the type + /// of the matching property in the last contract to preserve backward compatibility. + /// + /// + /// The override is applied when the generated and last-contract property types differ but + /// are logically compatible. Two categories are supported: + /// + /// + /// + /// Read-write collection properties ( or + /// ) whose element/key/value type names + /// match the last contract. This handles cases such as IReadOnlyList<T> + /// being regenerated as IList<T>. + /// + /// + /// + /// + /// All other public properties whose top-level type name (including any generic + /// argument names) matches the last contract. This handles cases such as a property + /// previously generated as a nullable scalar being regenerated as non-nullable, or a + /// property whose type is sourced from a different assembly but represents the same + /// logical type. + /// + /// + /// + /// + /// The property generated from the current input spec. + /// + /// When the method returns true, the type from the last contract that should be + /// used to override the generated property type. + /// + /// + /// true if the generated property type should be replaced with the last contract's + /// type; otherwise false. + /// + private bool TryGetLastContractPropertyTypeOverride( + PropertyProvider outputProperty, + [NotNullWhen(true)] out CSharpType? lastContractPropertyType) + { + lastContractPropertyType = null; + if (!LastContractPropertiesMap.TryGetValue(outputProperty.Name, out var candidate)) + { + return false; + } + + if (outputProperty.Type.Equals(candidate)) + { + return false; + } + + // Read-write collections: allow the wrapper (e.g. IList vs IReadOnlyList) to + // change as long as the element/key/value type names still match. We compare + // Arguments by name (not just ElementType) so this covers both list element types + // and dictionary key/value types. AreNamesEqual is used rather than Equals because + // the argument types may come from different sources (TypeProvider vs compiled + // assembly) but represent the same logical type. + if (outputProperty.Type.IsReadWriteList || outputProperty.Type.IsReadWriteDictionary) + { + if (outputProperty.Type.Arguments.Count == candidate.Arguments.Count && + outputProperty.Type.Arguments.Zip(candidate.Arguments).All( + pair => pair.First.AreNamesEqual(pair.Second))) + { + lastContractPropertyType = candidate; + return true; + } + + return false; + } + + // Other properties: require the entire type name (top-level plus any generic + // argument names) to match. This ensures we only override when the types are + // logically the same (e.g. differ only in nullability) and never when the + // underlying type has genuinely changed (e.g. string to int). + if (outputProperty.Type.AreNamesEqual(candidate)) + { + lastContractPropertyType = candidate; + return true; + } + + return false; + } + /// /// Determines whether to use object type for AdditionalProperties based on backward compatibility requirements. /// Checks if the last contract (previous version) had an AdditionalProperties property of type IDictionary<string, object>. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs index 286239e1663..6cec1e49991 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs @@ -1134,6 +1134,92 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.IsTrue(moreItemsProperty!.Type.Equals(typeof(IDictionary))); } + [Test] + public async Task BackCompat_NullableScalarPropertyTypeIsRetained() + { + // Regression: when a scalar property was previously generated as nullable + // but the current spec marks it as non-nullable, the previous nullable type + // should be preserved to avoid a source-breaking change. + var inputModel = InputFactory.Model( + "MockInputModel", + properties: + [ + InputFactory.Property("count", InputPrimitiveType.Int32, isRequired: true), + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; + Assert.IsNotNull(modelProvider); + + var countProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Count"); + Assert.IsNotNull(countProperty); + // The current spec says non-nullable int, but the last contract had int? – the + // generator should preserve the nullable type for backwards compatibility. + Assert.IsTrue(countProperty!.Type.Equals(new CSharpType(typeof(int), isNullable: true))); + } + + [Test] + public async Task BackCompat_ScalarPropertyTypeNotOverriddenWhenTypeNameDiffers() + { + // When the top-level property type name differs between the last contract and the + // current spec (e.g. string vs int), the generator must not silently replace the + // spec-defined type with the last contract's type. + var inputModel = InputFactory.Model( + "MockInputModel", + properties: + [ + InputFactory.Property("count", InputPrimitiveType.Int32, isRequired: true), + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; + Assert.IsNotNull(modelProvider); + + var countProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Count"); + Assert.IsNotNull(countProperty); + // Last contract has `string Count { get; set; }` but the new spec says int – the + // generator must not override the new type since the names differ entirely. + Assert.IsTrue(countProperty!.Type.Equals(typeof(int))); + } + + [Test] + public async Task BackCompat_EnumPropertyTypeIsRetainedWhenNullabilityDiffers() + { + // A scalar (non-collection) enum property whose nullability changed between the + // last contract and the current spec should retain the last contract's nullability. + var statusEnum = InputFactory.StringEnum( + "StatusEnum", + [("Active", "Active"), ("Inactive", "Inactive")], + isExtensible: true); + var inputModel = InputFactory.Model( + "MockInputModel", + properties: + [ + InputFactory.Property("status", statusEnum, isRequired: true), + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + inputEnumTypes: [statusEnum], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; + Assert.IsNotNull(modelProvider); + + var statusProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Status"); + Assert.IsNotNull(statusProperty); + // The last contract had StatusEnum? but the spec marks it required/non-nullable – + // the generator should preserve the nullable type to avoid a breaking change. + Assert.IsTrue(statusProperty!.Type.IsNullable); + Assert.AreEqual("StatusEnum", statusProperty.Type.Name); + } + [Test] public async Task BackCompat_NonAbstractTypeIsRespected() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_EnumPropertyTypeIsRetainedWhenNullabilityDiffers/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_EnumPropertyTypeIsRetainedWhenNullabilityDiffers/MockInputModel.cs new file mode 100644 index 00000000000..e9677060506 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_EnumPropertyTypeIsRetainedWhenNullabilityDiffers/MockInputModel.cs @@ -0,0 +1,11 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + public StatusEnum? Status { get; set; } + } + + public partial struct StatusEnum + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_NullableScalarPropertyTypeIsRetained/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_NullableScalarPropertyTypeIsRetained/MockInputModel.cs new file mode 100644 index 00000000000..1507b180d81 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_NullableScalarPropertyTypeIsRetained/MockInputModel.cs @@ -0,0 +1,7 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + public int? Count { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ScalarPropertyTypeNotOverriddenWhenTypeNameDiffers/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ScalarPropertyTypeNotOverriddenWhenTypeNameDiffers/MockInputModel.cs new file mode 100644 index 00000000000..c37eb2dd8db --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ScalarPropertyTypeNotOverriddenWhenTypeNameDiffers/MockInputModel.cs @@ -0,0 +1,7 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + public string Count { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md index 6858d0ba88d..92cbbfacbc0 100644 --- a/packages/http-client-csharp/generator/docs/backward-compatibility.md +++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md @@ -110,7 +110,15 @@ public static PublicModel1 PublicModel1( ### Model Properties -The generator attempts to maintain backward compatibility for model property types, particularly for collection types. +The generator attempts to maintain backward compatibility for all public model property types by preserving the previous property type when it differs from the type that would be generated from the current spec but is still logically compatible. + +The following scenarios are supported for any public model property: + +- **Collection wrapper change:** A property previously generated as a read-only collection (e.g. `IReadOnlyList` / `IReadOnlyDictionary`) that is now produced as a read-write collection (e.g. `IList` / `IDictionary`), or vice versa. The collection element/key/value type names must still match – the wrapper is preserved but genuine element-type changes are honoured. +- **Nullability change:** A scalar, enum, or model property that was previously generated as nullable (e.g. `int?`, `StatusEnum?`) and is now produced as non-nullable (or vice versa). The last contract's nullability is preserved when the top-level type name (and any generic argument names) still matches. +- **Same-name types from different sources:** Properties whose generated type is logically the same as the last contract's type, but sourced from different assemblies (e.g. a `TypeProvider`-produced type vs. a compiled-assembly type). Equality is evaluated by name rather than identity so these are treated as the same type. + +The override is **not** applied when the top-level property type names differ (for example a property that changed from `string` to `int`) – such changes are passed through so that the new spec is honoured. #### Scenario: Collection Property Type Changed @@ -136,10 +144,35 @@ public IList Items { get; set; } public IReadOnlyList Items { get; } ``` +#### Scenario: Scalar/Model Property Nullability Changed + +**Description:** When the nullability of a scalar, enum, or model property differs between the last contract and the current spec, the generator preserves the last contract's nullability to avoid a source-breaking change. + +**Example:** + +Previous version: + +```csharp +public int? Count { get; set; } +``` + +Current TypeSpec would generate: + +```csharp +public int Count { get; set; } +``` + +**Result:** The generator detects the nullability mismatch and preserves the previous nullable type: + +```csharp +public int? Count { get; set; } +``` + **Implementation Details:** -- The generator compares property types against the `LastContractView` -- For read-write lists and dictionaries, if the previous type was different, the previous type is retained +- The generator compares property types against the `LastContractView`. +- For read-write lists and dictionaries, if the previous type was different but the element/key/value type names match, the previous type is retained. +- For all other properties, if the previous type's top-level name (including any generic argument names) matches the new type's top-level name, the previous type is retained – this covers nullability changes and same-name types sourced from different assemblies. - A diagnostic message is logged: `"Changed property {ModelName}.{PropertyName} type to {LastContractType} to match last contract."` ### AdditionalProperties Type Preservation From fd9ef6e53af63b7b438ecb240ebbc3c5275b6c2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 22:05:01 +0000 Subject: [PATCH 03/15] Add BuildPropertiesForBackCompatibility API, align docs with main Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/e014a173-0713-44e4-a0b5-4ce8f896d407 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 49 +++++++-------- .../src/Providers/TypeProvider.cs | 10 ++++ .../generator/docs/backward-compatibility.md | 60 +++++++++++++++---- 3 files changed, 84 insertions(+), 35 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index 72339f66319..4928d22f3a5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -536,15 +536,6 @@ protected internal override PropertyProvider[] BuildProperties() continue; } - // Backcompat fix for property types: if a property existed in the last contract - // with a compatible but non-identical type, retain the previous type to avoid - // breaking consumers of the library. - if (TryGetLastContractPropertyTypeOverride(outputProperty, out var lastContractPropertyType)) - { - outputProperty.Type = lastContractPropertyType.ApplyInputSpecProperty(property); - CodeModelGenerator.Instance.Emitter.Info($"Changed property {Name}.{outputProperty.Name} type to {lastContractPropertyType} to match last contract."); - } - if (!isDiscriminator) { var derivedProperty = InputDerivedProperties.FirstOrDefault(p => p.Value.ContainsKey(property.Name)).Value?[property.Name]; @@ -582,7 +573,7 @@ protected internal override PropertyProvider[] BuildProperties() properties.AddRange(AdditionalPropertyProperties); } - return [.. properties]; + return [.. BuildPropertiesForBackCompatibility(properties)]; } private IEnumerable EnumerateBaseModels() @@ -1267,8 +1258,9 @@ _ when type.Equals(_additionalPropsUnknownType, ignoreNullable: true) => type, } /// - /// Determines whether the type of a generated property should be replaced with the type - /// of the matching property in the last contract to preserve backward compatibility. + /// Rewrites property types so that, when a property exists in the last contract with a + /// compatible but non-identical type, the previous type is preserved. This avoids + /// source-breaking changes for consumers of the library. /// /// /// The override is applied when the generated and last-contract property types differ but @@ -1286,22 +1278,31 @@ _ when type.Equals(_additionalPropsUnknownType, ignoreNullable: true) => type, /// /// All other public properties whose top-level type name (including any generic /// argument names) matches the last contract. This handles cases such as a property - /// previously generated as a nullable scalar being regenerated as non-nullable, or a - /// property whose type is sourced from a different assembly but represents the same - /// logical type. + /// previously generated as a nullable scalar being regenerated as non-nullable. /// /// /// /// - /// The property generated from the current input spec. - /// - /// When the method returns true, the type from the last contract that should be - /// used to override the generated property type. - /// - /// - /// true if the generated property type should be replaced with the last contract's - /// type; otherwise false. - /// + protected internal override IReadOnlyList BuildPropertiesForBackCompatibility(IEnumerable originalProperties) + { + var properties = originalProperties as IReadOnlyList ?? [.. originalProperties]; + if (LastContractPropertiesMap.Count == 0) + { + return properties; + } + + foreach (var outputProperty in properties) + { + if (TryGetLastContractPropertyTypeOverride(outputProperty, out var lastContractPropertyType)) + { + outputProperty.Type = lastContractPropertyType.ApplyInputSpecProperty(outputProperty.InputProperty); + CodeModelGenerator.Instance.Emitter.Info($"Changed property {Name}.{outputProperty.Name} type to {lastContractPropertyType} to match last contract."); + } + } + + return properties; + } + private bool TryGetLastContractPropertyTypeOverride( PropertyProvider outputProperty, [NotNullWhen(true)] out CSharpType? lastContractPropertyType) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index e34c6b33e35..c4c042fb808 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -613,6 +613,16 @@ protected internal virtual IReadOnlyList BuildMethodsForBackComp protected internal virtual IReadOnlyList BuildConstructorsForBackCompatibility(IEnumerable originalConstructors) => [.. originalConstructors]; + /// + /// Called from to apply backward-compatibility adjustments + /// to the set of properties produced for this type. Overrides can replace, reorder, or + /// otherwise rewrite properties based on the . + /// + /// The properties as produced from the current input spec. + /// The possibly-adjusted list of properties. + protected internal virtual IReadOnlyList BuildPropertiesForBackCompatibility(IEnumerable originalProperties) + => [.. originalProperties]; + private IReadOnlyList? _enumValues; private bool ShouldGenerate(ConstructorProvider constructor) diff --git a/packages/http-client-csharp/generator/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md index 92cbbfacbc0..9c2bd4163e2 100644 --- a/packages/http-client-csharp/generator/docs/backward-compatibility.md +++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md @@ -14,6 +14,8 @@ - [Parameter Naming](#parameter-naming) - [Page Size Parameter Casing Correction](#scenario-page-size-parameter-casing-correction) - [Top Parameter Conversion to MaxCount](#scenario-top-parameter-conversion-to-maxcount) + - [Content-Type Parameter Ordering](#content-type-parameter-ordering) + - [Content-Type Before Body Preserved from Last Contract](#scenario-content-type-before-body-preserved-from-last-contract) ## Overview @@ -110,15 +112,7 @@ public static PublicModel1 PublicModel1( ### Model Properties -The generator attempts to maintain backward compatibility for all public model property types by preserving the previous property type when it differs from the type that would be generated from the current spec but is still logically compatible. - -The following scenarios are supported for any public model property: - -- **Collection wrapper change:** A property previously generated as a read-only collection (e.g. `IReadOnlyList` / `IReadOnlyDictionary`) that is now produced as a read-write collection (e.g. `IList` / `IDictionary`), or vice versa. The collection element/key/value type names must still match – the wrapper is preserved but genuine element-type changes are honoured. -- **Nullability change:** A scalar, enum, or model property that was previously generated as nullable (e.g. `int?`, `StatusEnum?`) and is now produced as non-nullable (or vice versa). The last contract's nullability is preserved when the top-level type name (and any generic argument names) still matches. -- **Same-name types from different sources:** Properties whose generated type is logically the same as the last contract's type, but sourced from different assemblies (e.g. a `TypeProvider`-produced type vs. a compiled-assembly type). Equality is evaluated by name rather than identity so these are treated as the same type. - -The override is **not** applied when the top-level property type names differ (for example a property that changed from `string` to `int`) – such changes are passed through so that the new spec is honoured. +The generator attempts to maintain backward compatibility for model property types by preserving the previous property type whenever it differs from the type produced by the current spec but is still logically compatible. This applies to all public model properties (scalars, enums, models, and collections). #### Scenario: Collection Property Type Changed @@ -171,8 +165,8 @@ public int? Count { get; set; } **Implementation Details:** - The generator compares property types against the `LastContractView`. -- For read-write lists and dictionaries, if the previous type was different but the element/key/value type names match, the previous type is retained. -- For all other properties, if the previous type's top-level name (including any generic argument names) matches the new type's top-level name, the previous type is retained – this covers nullability changes and same-name types sourced from different assemblies. +- If the previous type is logically compatible with the new type (same top-level name, with matching generic argument names), the previous type is retained. This covers read-write collection wrapper changes (e.g. `IList` ↔ `IReadOnlyList`) as well as nullability changes on scalars, enums, and models. +- If the top-level property type names differ entirely (for example `string` → `int`), the new spec is honoured and no override is applied. - A diagnostic message is logged: `"Changed property {ModelName}.{PropertyName} type to {LastContractType} to match last contract."` ### AdditionalProperties Type Preservation @@ -623,3 +617,47 @@ public virtual AsyncPageable GetItemsAsync(int? maxCount = null, Cancellat - This conversion is specific to paging operations only - Existing client code with `top` continues to compile without changes - New code benefits from the standardized `maxCount` naming convention + +### Content-Type Parameter Ordering + +The generator places the `contentType` parameter after the body (`content`) parameter in method signatures. However, backward compatibility is maintained when the last contract had a different ordering. + +#### Scenario: Content-Type Before Body Preserved from Last Contract + +**Description:** The generator places `contentType` after the `content` (body) parameter. However, if the last contract had `contentType` before `content`, the generator preserves that ordering to avoid breaking existing code. + +This commonly occurs when a library was previously generated with contentType before body and has already been released (GA'd). + +**Example:** + +**contentType before body exists in LastContractView - preserved for backward compatibility** + +Previous version had `contentType` before `content`: + +```csharp +public virtual ClientResult UpdateSkillDefaultVersion(string skillId, string contentType, BinaryContent content, RequestOptions options = null) +{ + // ... +} +``` + +Current TypeSpec defines a content type: + +```typespec +op UpdateSkillDefaultVersion( + @path skill_id: string, + @header contentType: string, + @body body: SetDefaultSkillVersionBody, +): SkillResource; +``` + +**Generated Compatibility Result:** + +The generator detects that the previous contract had `contentType` before `content` and preserves that ordering: + +```csharp +public virtual ClientResult UpdateSkillDefaultVersion(string skillId, string contentType, BinaryContent content, RequestOptions options = null) +{ + // contentType stays before content for backward compatibility +} +``` From 6275b900c07210b83a692bca4a741eda868dbc89 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:43:29 +0000 Subject: [PATCH 04/15] Merge main: align with #10319 revert (drop AreNamesEqual on collection back-compat) Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/1cb571b4-94fe-450a-bda0-73a4c98e8f9f Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- .../eng/pipeline/publish.yml | 7 + .../src/Providers/RestClientProvider.cs | 66 ++++++++- .../RestClientProviderTests.cs | 129 ++++++++++++++++++ .../TestClient.cs | 15 ++ .../TestClient.cs | 15 ++ .../src/Providers/ModelFactoryProvider.cs | 2 +- .../src/Providers/ModelProvider.cs | 29 ++-- .../ModelFactoryProviderTests.cs | 49 ++++++- .../SampleNamespaceModelFactory.cs | 26 ++++ .../ModelProviders/ModelProviderTests.cs | 45 ------ .../MockInputModel.cs | 10 -- .../generator/docs/backward-compatibility.md | 4 +- 12 files changed, 316 insertions(+), 81 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeAfterBodyInLastContractView/TestClient.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeOrderPreservedFromLastContractView/TestClient.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_NewPropertyAddedWithDifferentParamOrder/SampleNamespaceModelFactory.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_CollectionPropertyTypeNotOverriddenWhenElementTypeChanges/MockInputModel.cs diff --git a/packages/http-client-csharp/eng/pipeline/publish.yml b/packages/http-client-csharp/eng/pipeline/publish.yml index 9e953867b38..fd1cf5ace9e 100644 --- a/packages/http-client-csharp/eng/pipeline/publish.yml +++ b/packages/http-client-csharp/eng/pipeline/publish.yml @@ -106,6 +106,12 @@ extends: jobs: - job: CreatePR timeoutInMinutes: 90 + variables: + # Redirect temp and cache directories to Agent.TempDirectory (a separate, larger partition) + # to avoid running out of disk space on the root partition during generation + TMPDIR: $(Agent.TempDirectory) + NUGET_PACKAGES: $(Agent.TempDirectory)/nuget + npm_config_cache: $(Agent.TempDirectory)/npm-cache steps: - checkout: self - pwsh: | @@ -150,6 +156,7 @@ extends: workingFile: $(Build.SourcesDirectory)/packages/http-client-csharp/.npmrc - download: current + artifact: build_artifacts_csharp displayName: Download pipeline artifacts - pwsh: | diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 88e710a5373..44fd9b7d549 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -1007,6 +1007,7 @@ internal static List GetMethodParameters( int required = 100; int bodyRequired = 200; int bodyOptional = 300; + int contentType = 350; int optional = 400; var operation = serviceMethod.Operation; @@ -1124,7 +1125,12 @@ internal static List GetMethodParameters( break; case ParameterLocation.Query: case ParameterLocation.Header: - if (parameter.DefaultValue == null) + if (inputParam is InputHeaderParameter { IsContentType: true } + && !HasContentTypeBeforeBodyInLastContract(serviceMethod.Name, client.BackCompatProvider)) + { + sortedParams.Add(contentType++, parameter); + } + else if (parameter.DefaultValue == null) { sortedParams.Add(required++, parameter); } @@ -1144,7 +1150,7 @@ internal static List GetMethodParameters( if (operation.IsMultipartFormData) { - sortedParams.Add(bodyRequired++, ScmKnownParameters.ContentType); + sortedParams.Add(contentType++, ScmKnownParameters.ContentType); } if (methodType == ScmMethodKind.CreateRequest) @@ -1159,6 +1165,62 @@ internal static List GetMethodParameters( return [.. sortedParams.Values]; } + /// + /// Checks if the last contract view contains a method matching the given name where + /// a "contentType" parameter appears before the body ("content") parameter. + /// If so, we should preserve that ordering for backward compatibility. + /// + private static bool HasContentTypeBeforeBodyInLastContract(string methodName, TypeProvider backCompatProvider) + { + const string contentTypeParamName = "contentType"; + const string contentParamName = "content"; + + var lastContractMethods = backCompatProvider.LastContractView?.Methods; + if (lastContractMethods == null || lastContractMethods.Count == 0) + { + return false; + } + + var syncMethodName = methodName; + var asyncMethodName = methodName + "Async"; + + foreach (var method in lastContractMethods) + { + if (!string.Equals(method.Signature.Name, syncMethodName, StringComparison.OrdinalIgnoreCase) + && !string.Equals(method.Signature.Name, asyncMethodName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + int contentTypeIndex = -1; + int bodyIndex = -1; + for (int i = 0; i < method.Signature.Parameters.Count; i++) + { + var param = method.Signature.Parameters[i]; + if (string.Equals(param.Name, contentTypeParamName, StringComparison.OrdinalIgnoreCase)) + { + contentTypeIndex = i; + } + else if (string.Equals(param.Name, contentParamName, StringComparison.OrdinalIgnoreCase)) + { + bodyIndex = i; + } + + if (contentTypeIndex >= 0 && bodyIndex >= 0) + { + break; + } + } + + if (contentTypeIndex >= 0 && bodyIndex >= 0 && contentTypeIndex < bodyIndex) + { + return true; + } + } + + return false; + } + internal static InputModelType GetSpreadParameterModel(InputParameter inputParam) { if (inputParam.Type is InputModelType model) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs index 2a9638dab4b..5ae9a8a010a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs @@ -233,6 +233,135 @@ public void TestGetMethodParameters_ProperOrdering() Assert.AreEqual("b", orderedPathParams[2].Name); } + [TestCase(true, false)] + [TestCase(false, false)] + [TestCase(true, true)] + [TestCase(false, true)] + public void TestGetMethodParameters_ContentTypeAfterBody(bool isRequired, bool isExtensibleEnum) + { + InputType contentTypeType = isExtensibleEnum + ? InputFactory.StringEnum("ContentTypeEnum", + [("application/json", "application/json"), ("application/xml", "application/xml")], + isExtensible: true) + : InputPrimitiveType.String; + var contentTypeHeader = InputFactory.HeaderParameter( + "contentType", + contentTypeType, + isRequired: isRequired, + isContentType: true, + serializedName: "Content-Type", + defaultValue: isRequired ? null : InputFactory.Constant.String("application/json")); + var bodyParam = InputFactory.BodyParameter("body", InputPrimitiveType.String, isRequired: true); + var pathParam = InputFactory.PathParameter("skillId", InputPrimitiveType.String, isRequired: true); + + var operation = InputFactory.Operation( + "UpdateSkillDefaultVersion", + parameters: [pathParam, contentTypeHeader, bodyParam]); + + var serviceMethod = InputFactory.BasicServiceMethod( + "UpdateSkillDefaultVersion", + operation); + + var inputClient = InputFactory.Client("TestClient", methods: [serviceMethod]); + var clientProvider = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient); + Assert.IsNotNull(clientProvider); + + var methodParameters = RestClientProvider.GetMethodParameters(serviceMethod, ScmMethodKind.Protocol, clientProvider!); + + Assert.AreEqual(3, methodParameters.Count); + Assert.AreEqual("skillId", methodParameters[0].Name); + Assert.AreEqual("content", methodParameters[1].Name); + Assert.AreEqual("contentType", methodParameters[2].Name); + } + + [Test] + public async Task ContentTypeOrderPreservedFromLastContractView() + { + // Create an operation with a content-type header (union) and body + var contentTypeEnum = InputFactory.StringEnum("ContentTypeEnum", + [("application/json", "application/json"), ("application/xml", "application/xml")], + isExtensible: true); + var contentTypeHeader = InputFactory.HeaderParameter( + "contentType", + contentTypeEnum, + isRequired: true, + isContentType: true, + serializedName: "Content-Type"); + var bodyParam = InputFactory.BodyParameter("body", InputPrimitiveType.String, isRequired: true); + var pathParam = InputFactory.PathParameter("skillId", InputPrimitiveType.String, isRequired: true); + + var operation = InputFactory.Operation( + "UpdateSkillDefaultVersion", + parameters: [pathParam, contentTypeHeader, bodyParam]); + + var serviceMethod = InputFactory.BasicServiceMethod( + "UpdateSkillDefaultVersion", + operation); + + var client = InputFactory.Client("TestClient", methods: [serviceMethod]); + + // Load with a last contract that has contentType before body + var generator = await MockHelpers.LoadMockGeneratorAsync( + clients: () => [client], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var clientProvider = generator.Object.OutputLibrary.TypeProviders.OfType().FirstOrDefault(); + Assert.IsNotNull(clientProvider); + Assert.IsNotNull(clientProvider!.LastContractView); + + var methodParameters = RestClientProvider.GetMethodParameters(serviceMethod, ScmMethodKind.Protocol, clientProvider!); + + // When the last contract had contentType before body, the ordering should be preserved + Assert.AreEqual(3, methodParameters.Count); + Assert.AreEqual("skillId", methodParameters[0].Name); + Assert.AreEqual("contentType", methodParameters[1].Name); // contentType stays before body for back-compat + Assert.AreEqual("content", methodParameters[2].Name); + } + + [Test] + public async Task ContentTypeAfterBodyInLastContractView() + { + // Create an operation with a content-type header (union) and body + var contentTypeEnum = InputFactory.StringEnum("ContentTypeEnum", + [("application/json", "application/json"), ("application/xml", "application/xml")], + isExtensible: true); + var contentTypeHeader = InputFactory.HeaderParameter( + "contentType", + contentTypeEnum, + isRequired: true, + isContentType: true, + serializedName: "Content-Type"); + var bodyParam = InputFactory.BodyParameter("body", InputPrimitiveType.String, isRequired: true); + var pathParam = InputFactory.PathParameter("skillId", InputPrimitiveType.String, isRequired: true); + + var operation = InputFactory.Operation( + "UpdateSkillDefaultVersion", + parameters: [pathParam, contentTypeHeader, bodyParam]); + + var serviceMethod = InputFactory.BasicServiceMethod( + "UpdateSkillDefaultVersion", + operation); + + var client = InputFactory.Client("TestClient", methods: [serviceMethod]); + + // Load with a last contract that already has contentType after body + var generator = await MockHelpers.LoadMockGeneratorAsync( + clients: () => [client], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var clientProvider = generator.Object.OutputLibrary.TypeProviders.OfType().FirstOrDefault(); + Assert.IsNotNull(clientProvider); + Assert.IsNotNull(clientProvider!.LastContractView); + + var methodParameters = RestClientProvider.GetMethodParameters(serviceMethod, ScmMethodKind.Protocol, clientProvider!); + + // When the last contract already had contentType after body, the new ordering is used + Assert.AreEqual(3, methodParameters.Count); + Assert.AreEqual("skillId", methodParameters[0].Name); + Assert.AreEqual("content", methodParameters[1].Name); + Assert.AreEqual("contentType", methodParameters[2].Name); // contentType after body + } + [TestCase(true, true)] [TestCase(true, false)] [TestCase(false, true)] diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeAfterBodyInLastContractView/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeAfterBodyInLastContractView/TestClient.cs new file mode 100644 index 00000000000..1848aa23746 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeAfterBodyInLastContractView/TestClient.cs @@ -0,0 +1,15 @@ +#nullable disable + +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + // This represents a previous contract where contentType already appears after body (content) + public virtual Task UpdateSkillDefaultVersionAsync(string skillId, BinaryContent content, string contentType, RequestOptions options = null) { return null; } + public virtual ClientResult UpdateSkillDefaultVersion(string skillId, BinaryContent content, string contentType, RequestOptions options = null) { return null; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeOrderPreservedFromLastContractView/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeOrderPreservedFromLastContractView/TestClient.cs new file mode 100644 index 00000000000..8ad616e02d8 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeOrderPreservedFromLastContractView/TestClient.cs @@ -0,0 +1,15 @@ +#nullable disable + +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + // This represents a previous contract where contentType appears before body (content) + public virtual Task UpdateSkillDefaultVersionAsync(string skillId, string contentType, BinaryContent content, RequestOptions options = null) { return null; } + public virtual ClientResult UpdateSkillDefaultVersion(string skillId, string contentType, BinaryContent content, RequestOptions options = null) { return null; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs index a61da49e42b..c4ed418c3b6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs @@ -286,7 +286,7 @@ private static bool TryBuildMethodArgumentsForOverload( } else { - arguments.Add(previousParameter); + arguments.Add(parameter.PositionalReference(previousParameter)); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index 4928d22f3a5..a23f217d78e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -1263,15 +1263,14 @@ _ when type.Equals(_additionalPropsUnknownType, ignoreNullable: true) => type, /// source-breaking changes for consumers of the library. /// /// - /// The override is applied when the generated and last-contract property types differ but - /// are logically compatible. Two categories are supported: + /// The override is applied when the generated and last-contract property types differ. + /// Two categories are supported: /// /// /// /// Read-write collection properties ( or - /// ) whose element/key/value type names - /// match the last contract. This handles cases such as IReadOnlyList<T> - /// being regenerated as IList<T>. + /// ). This handles cases such as + /// IReadOnlyList<T> being regenerated as IList<T>. /// /// /// @@ -1318,23 +1317,13 @@ private bool TryGetLastContractPropertyTypeOverride( return false; } - // Read-write collections: allow the wrapper (e.g. IList vs IReadOnlyList) to - // change as long as the element/key/value type names still match. We compare - // Arguments by name (not just ElementType) so this covers both list element types - // and dictionary key/value types. AreNamesEqual is used rather than Equals because - // the argument types may come from different sources (TypeProvider vs compiled - // assembly) but represent the same logical type. + // Read-write collections: preserve the previous type whenever it differs (e.g. + // IReadOnlyList regenerated as IList). This matches the long-standing + // behavior in the inline back-compat block. if (outputProperty.Type.IsReadWriteList || outputProperty.Type.IsReadWriteDictionary) { - if (outputProperty.Type.Arguments.Count == candidate.Arguments.Count && - outputProperty.Type.Arguments.Zip(candidate.Arguments).All( - pair => pair.First.AreNamesEqual(pair.Second))) - { - lastContractPropertyType = candidate; - return true; - } - - return false; + lastContractPropertyType = candidate; + return true; } // Other properties: require the entire type name (top-level plus any generic diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs index 52e0ffb88a8..ec7ad89d221 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs @@ -193,7 +193,54 @@ public async Task BackCompatibility_NewModelPropertyAdded() Assert.IsNotNull(body); var result = body!.ToDisplayString(); Assert.AreEqual( - "return PublicModel1(stringProp, modelProp, listProp, dictProp: default);\n", + "return PublicModel1(stringProp: stringProp, modelProp: modelProp, listProp: listProp, dictProp: default);\n", + result); + } + + // This test validates that when a new property is added AND the previous contract had a different + // parameter ordering, the backward-compat overload uses named arguments to correctly call the current method. + [Test] + public async Task BackCompatibility_NewPropertyAddedWithDifferentParamOrder() + { + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: ModelList, + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync())).Object; + + var modelFactory = _instance!.OutputLibrary.ModelFactory.Value; + Assert.AreEqual("SampleNamespaceModelFactory", modelFactory.Name); + + modelFactory.ProcessTypeForBackCompatibility(); + + var methods = modelFactory.Methods; + // There should be an additional method for backward compatibility + Assert.AreEqual(ModelList.Length - ModelList.Where(m => m.Access == "internal").Count() + 1, methods.Count); + + var currentOverloadMethod = methods + .FirstOrDefault(m => m.Signature.Name == "PublicModel1" && m.Signature.Parameters.Any(p => p.Name == "dictProp")); + var backwardCompatibilityMethod = methods + .FirstOrDefault(m => m.Signature.Name == "PublicModel1" && m.Signature.Parameters.All(p => p.Name != "dictProp")); + Assert.IsNotNull(currentOverloadMethod); + Assert.IsNotNull(backwardCompatibilityMethod); + + // validate the signature of the backward compatibility method preserves the previous parameter order + var parameters = backwardCompatibilityMethod!.Signature.Parameters; + Assert.AreEqual(3, parameters.Count); + Assert.AreEqual("modelProp", parameters[0].Name); + Assert.AreEqual("stringProp", parameters[1].Name); + Assert.AreEqual("listProp", parameters[2].Name); + foreach (var param in parameters) + { + Assert.IsNull(param.DefaultValue); + } + + // validate the previous method body uses named arguments to ensure correct mapping + // even though the parameter order differs between the previous and current methods + var body = backwardCompatibilityMethod!.BodyStatements; + Assert.IsNotNull(body); + var result = body!.ToDisplayString(); + Assert.AreEqual( + "return PublicModel1(stringProp: stringProp, modelProp: modelProp, listProp: listProp, dictProp: default);\n", result); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_NewPropertyAddedWithDifferentParamOrder/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_NewPropertyAddedWithDifferentParamOrder/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..10a9b59591d --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_NewPropertyAddedWithDifferentParamOrder/SampleNamespaceModelFactory.cs @@ -0,0 +1,26 @@ +using SampleTypeSpec; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static PublicModel1 PublicModel1( + Thing modelProp = default, + string stringProp = default, + IEnumerable listProp = default) + { } + } +} + +namespace Sample.Models +{ + public partial class PublicModel1 + { } + + public partial class Thing + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs index 6cec1e49991..560dffe1dd7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs @@ -1062,51 +1062,6 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.IsTrue(moreItemsProperty!.Type.Equals(new CSharpType(typeof(IReadOnlyDictionary<,>), typeof(string), elementEnumProvider.Type))); } - [Test] - public async Task BackCompat_CollectionPropertyTypeNotOverriddenWhenElementTypeChanges() - { - // Simulate the scenario where the element type of a collection property has changed - // (e.g., from Record[] to BulkVMConfiguration[] via @typeChangedFrom). - // The last contract has IList> but the new code model - // produces IList. The override should NOT apply because the element - // type has changed. - var newElementModel = InputFactory.Model( - "NewElementModel", - properties: - [ - InputFactory.Property("name", InputPrimitiveType.String) - ]); - var inputModel = InputFactory.Model( - "MockInputModel", - properties: - [ - InputFactory.Property("items", InputFactory.Array(newElementModel)), - InputFactory.Property("moreItems", InputFactory.Dictionary(newElementModel)) - ]); - - await MockHelpers.LoadMockGeneratorAsync( - inputModelTypes: [inputModel, newElementModel], - lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); - - var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; - Assert.IsNotNull(modelProvider); - - var newElementModelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "NewElementModel") as ModelProvider; - Assert.IsNotNull(newElementModelProvider); - - // The items property should use the new element type (IList), not be - // overridden to the old type (IList>) from last contract - var itemsProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Items"); - Assert.IsNotNull(itemsProperty); - Assert.IsTrue(itemsProperty!.Type.Equals(new CSharpType(typeof(IList<>), newElementModelProvider!.Type))); - - // The moreItems property should use the new element type (IDictionary), not be - // overridden to the old type (IDictionary>) from last contract - var moreItemsProperty = modelProvider.Properties.FirstOrDefault(p => p.Name == "MoreItems"); - Assert.IsNotNull(moreItemsProperty); - Assert.IsTrue(moreItemsProperty!.Type.Equals(new CSharpType(typeof(IDictionary<,>), typeof(string), newElementModelProvider.Type))); - } - [Test] public async Task BackCompat_InternalTypesAreIgnored() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_CollectionPropertyTypeNotOverriddenWhenElementTypeChanges/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_CollectionPropertyTypeNotOverriddenWhenElementTypeChanges/MockInputModel.cs deleted file mode 100644 index 9ab0d2614ef..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_CollectionPropertyTypeNotOverriddenWhenElementTypeChanges/MockInputModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; - -namespace Sample.Models -{ - public partial class MockInputModel - { - public IList> Items { get; } - public IDictionary> MoreItems { get; } - } -} diff --git a/packages/http-client-csharp/generator/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md index 9c2bd4163e2..02933609fcb 100644 --- a/packages/http-client-csharp/generator/docs/backward-compatibility.md +++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md @@ -165,8 +165,8 @@ public int? Count { get; set; } **Implementation Details:** - The generator compares property types against the `LastContractView`. -- If the previous type is logically compatible with the new type (same top-level name, with matching generic argument names), the previous type is retained. This covers read-write collection wrapper changes (e.g. `IList` ↔ `IReadOnlyList`) as well as nullability changes on scalars, enums, and models. -- If the top-level property type names differ entirely (for example `string` → `int`), the new spec is honoured and no override is applied. +- For read-write lists and dictionaries, if the previous type was different, the previous type is retained. +- For other public properties (scalars, enums, models), the previous type is retained when it differs from the new type only at the top level — for example when nullability changes (`int?` → `int`). If the top-level property type names differ entirely (for example `string` → `int`), the new spec is honoured and no override is applied. - A diagnostic message is logged: `"Changed property {ModelName}.{PropertyName} type to {LastContractType} to match last contract."` ### AdditionalProperties Type Preservation From 86c40d0fa9a87cffb7ff9d8e90494e7da6aaae91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:57:39 +0000 Subject: [PATCH 05/15] Always preserve last contract property type in back-compat scenario Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/111ae35d-3fde-4326-95a5-11485d35034c Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 26 +++++-------------- .../ModelProviders/ModelProviderTests.cs | 15 ++++++----- .../MockInputModel.cs | 0 .../generator/docs/backward-compatibility.md | 17 +++++------- 4 files changed, 20 insertions(+), 38 deletions(-) rename packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/{BackCompat_ScalarPropertyTypeNotOverriddenWhenTypeNameDiffers => BackCompat_ScalarPropertyTypeOverriddenWhenTypeNameDiffers}/MockInputModel.cs (100%) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index a23f217d78e..90c4f2013c6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -1317,26 +1317,12 @@ private bool TryGetLastContractPropertyTypeOverride( return false; } - // Read-write collections: preserve the previous type whenever it differs (e.g. - // IReadOnlyList regenerated as IList). This matches the long-standing - // behavior in the inline back-compat block. - if (outputProperty.Type.IsReadWriteList || outputProperty.Type.IsReadWriteDictionary) - { - lastContractPropertyType = candidate; - return true; - } - - // Other properties: require the entire type name (top-level plus any generic - // argument names) to match. This ensures we only override when the types are - // logically the same (e.g. differ only in nullability) and never when the - // underlying type has genuinely changed (e.g. string to int). - if (outputProperty.Type.AreNamesEqual(candidate)) - { - lastContractPropertyType = candidate; - return true; - } - - return false; + // Always preserve the last contract's property type when it differs from the + // type produced by the current spec. This prevents source-breaking changes + // for any kind of property change (collection wrapper, nullability, underlying + // type, etc.). Users can override this behavior with custom code if needed. + lastContractPropertyType = candidate; + return true; } /// diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs index 560dffe1dd7..b6b2cdd1113 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs @@ -1117,11 +1117,12 @@ await MockHelpers.LoadMockGeneratorAsync( } [Test] - public async Task BackCompat_ScalarPropertyTypeNotOverriddenWhenTypeNameDiffers() + public async Task BackCompat_ScalarPropertyTypeOverriddenWhenTypeNameDiffers() { - // When the top-level property type name differs between the last contract and the - // current spec (e.g. string vs int), the generator must not silently replace the - // spec-defined type with the last contract's type. + // When the property type differs between the last contract and the current spec + // (including a top-level type name change like string vs int), the generator + // preserves the last contract's type to avoid a source-breaking change. Users + // can override this behavior with custom code if needed. var inputModel = InputFactory.Model( "MockInputModel", properties: @@ -1138,9 +1139,9 @@ await MockHelpers.LoadMockGeneratorAsync( var countProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Count"); Assert.IsNotNull(countProperty); - // Last contract has `string Count { get; set; }` but the new spec says int – the - // generator must not override the new type since the names differ entirely. - Assert.IsTrue(countProperty!.Type.Equals(typeof(int))); + // Last contract has `string Count { get; set; }` and the new spec says int – the + // generator preserves the last contract's type for backwards compatibility. + Assert.IsTrue(countProperty!.Type.Equals(typeof(string))); } [Test] diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ScalarPropertyTypeNotOverriddenWhenTypeNameDiffers/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ScalarPropertyTypeOverriddenWhenTypeNameDiffers/MockInputModel.cs similarity index 100% rename from packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ScalarPropertyTypeNotOverriddenWhenTypeNameDiffers/MockInputModel.cs rename to packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ScalarPropertyTypeOverriddenWhenTypeNameDiffers/MockInputModel.cs diff --git a/packages/http-client-csharp/generator/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md index 02933609fcb..89eb0e3cc88 100644 --- a/packages/http-client-csharp/generator/docs/backward-compatibility.md +++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md @@ -112,11 +112,11 @@ public static PublicModel1 PublicModel1( ### Model Properties -The generator attempts to maintain backward compatibility for model property types by preserving the previous property type whenever it differs from the type produced by the current spec but is still logically compatible. This applies to all public model properties (scalars, enums, models, and collections). +The generator preserves the previous property type whenever it differs from the type produced by the current spec. This applies to all public model properties (scalars, enums, models, and collections), so any property type change is non-source-breaking by default. Users who want the new spec's type to take effect can override this behavior with custom code. #### Scenario: Collection Property Type Changed -**Description:** When a property type changes from a read-only collection to a read-write collection (or vice versa), the generator attempts to preserve the previous property type to avoid breaking changes. +**Description:** When a property type changes from a read-only collection to a read-write collection (or vice versa), the generator preserves the previous property type to avoid breaking changes. **Example:** @@ -138,9 +138,9 @@ public IList Items { get; set; } public IReadOnlyList Items { get; } ``` -#### Scenario: Scalar/Model Property Nullability Changed +#### Scenario: Scalar/Model Property Type Changed -**Description:** When the nullability of a scalar, enum, or model property differs between the last contract and the current spec, the generator preserves the last contract's nullability to avoid a source-breaking change. +**Description:** When the type of a scalar, enum, or model property differs between the last contract and the current spec — whether the change is in nullability, the underlying type, or anything else — the generator preserves the last contract's type. **Example:** @@ -156,18 +156,13 @@ Current TypeSpec would generate: public int Count { get; set; } ``` -**Result:** The generator detects the nullability mismatch and preserves the previous nullable type: +**Result:** The generator detects the type mismatch and preserves the previous nullable type: ```csharp public int? Count { get; set; } ``` -**Implementation Details:** - -- The generator compares property types against the `LastContractView`. -- For read-write lists and dictionaries, if the previous type was different, the previous type is retained. -- For other public properties (scalars, enums, models), the previous type is retained when it differs from the new type only at the top level — for example when nullability changes (`int?` → `int`). If the top-level property type names differ entirely (for example `string` → `int`), the new spec is honoured and no override is applied. -- A diagnostic message is logged: `"Changed property {ModelName}.{PropertyName} type to {LastContractType} to match last contract."` +A diagnostic message is logged for every overridden property: `"Changed property {ModelName}.{PropertyName} type to {LastContractType} to match last contract."` ### AdditionalProperties Type Preservation From f4cdb930f04ca5630d0152b8726491c5091ef3fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:35:13 +0000 Subject: [PATCH 06/15] Simplify TryGetLastContractPropertyTypeOverride; sync from main Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/ceb10cde-230f-420c-9ac3-076d097c12b6 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index 90c4f2013c6..414c77bb853 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -1306,23 +1306,19 @@ private bool TryGetLastContractPropertyTypeOverride( PropertyProvider outputProperty, [NotNullWhen(true)] out CSharpType? lastContractPropertyType) { - lastContractPropertyType = null; - if (!LastContractPropertiesMap.TryGetValue(outputProperty.Name, out var candidate)) - { - return false; - } - - if (outputProperty.Type.Equals(candidate)) - { - return false; - } - // Always preserve the last contract's property type when it differs from the // type produced by the current spec. This prevents source-breaking changes // for any kind of property change (collection wrapper, nullability, underlying // type, etc.). Users can override this behavior with custom code if needed. - lastContractPropertyType = candidate; - return true; + lastContractPropertyType = null; + if (LastContractPropertiesMap.TryGetValue(outputProperty.Name, out var candidate) && + !candidate.Equals(outputProperty.Type)) + { + lastContractPropertyType = candidate; + return true; + } + + return false; } /// From 4584e8b2120cc10e06ab44ffdcda17169838f9d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:40:05 +0000 Subject: [PATCH 07/15] Update BuildPropertiesForBackCompatibility XML summary to reflect unconditional preservation Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/aa2dc7c2-4537-41bc-823e-a6c608afa956 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 29 ++++--------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index 414c77bb853..59a5fc17c40 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -1258,30 +1258,13 @@ _ when type.Equals(_additionalPropsUnknownType, ignoreNullable: true) => type, } /// - /// Rewrites property types so that, when a property exists in the last contract with a - /// compatible but non-identical type, the previous type is preserved. This avoids - /// source-breaking changes for consumers of the library. + /// Rewrites property types so that, whenever a property exists in the last contract + /// with a different type than the one produced by the current spec, the previous + /// contract's type is preserved. This avoids source-breaking changes for consumers + /// of the library for any kind of property change (collection wrapper, nullability, + /// underlying type, etc.). Users can override this behavior with custom code if they + /// want the new spec's type instead. /// - /// - /// The override is applied when the generated and last-contract property types differ. - /// Two categories are supported: - /// - /// - /// - /// Read-write collection properties ( or - /// ). This handles cases such as - /// IReadOnlyList<T> being regenerated as IList<T>. - /// - /// - /// - /// - /// All other public properties whose top-level type name (including any generic - /// argument names) matches the last contract. This handles cases such as a property - /// previously generated as a nullable scalar being regenerated as non-nullable. - /// - /// - /// - /// protected internal override IReadOnlyList BuildPropertiesForBackCompatibility(IEnumerable originalProperties) { var properties = originalProperties as IReadOnlyList ?? [.. originalProperties]; From 0f006de4ecf0e0996eff3199734117991e22a0d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 00:22:08 +0000 Subject: [PATCH 08/15] Move BuildPropertiesForBackCompatibility to run after visitors Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/4980a671-b6b7-46c0-a820-196ce0d078fa Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 2 +- .../src/Providers/TypeProvider.cs | 11 +++++++---- .../Providers/ModelProviders/ModelProviderTests.cs | 7 +++++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index 59a5fc17c40..19c5e4be86a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -573,7 +573,7 @@ protected internal override PropertyProvider[] BuildProperties() properties.AddRange(AdditionalPropertyProperties); } - return [.. BuildPropertiesForBackCompatibility(properties)]; + return [.. properties]; } private IEnumerable EnumerateBaseModels() diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index c4c042fb808..052a4c84157 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -579,6 +579,7 @@ internal void ProcessTypeForBackCompatibility() { var hasMethods = LastContractView?.Methods != null && LastContractView.Methods.Count > 0; var hasConstructors = LastContractView?.Constructors != null && LastContractView.Constructors.Count > 0; + var hasProperties = LastContractView?.Properties != null && LastContractView.Properties.Count > 0; IEnumerable? newFields = null; if (this is EnumProvider) @@ -597,10 +598,11 @@ internal void ProcessTypeForBackCompatibility() var newMethods = hasMethods ? BuildMethodsForBackCompatibility(Methods) : null; var newConstructors = hasConstructors ? BuildConstructorsForBackCompatibility(Constructors) : null; + var newProperties = hasProperties ? BuildPropertiesForBackCompatibility(Properties) : null; - if (newFields != null || newMethods != null || newConstructors != null) + if (newFields != null || newMethods != null || newConstructors != null || newProperties != null) { - Update(fields: newFields, methods: newMethods, constructors: newConstructors); + Update(fields: newFields, methods: newMethods, constructors: newConstructors, properties: newProperties); } } @@ -614,8 +616,9 @@ protected internal virtual IReadOnlyList BuildConstructorsF => [.. originalConstructors]; /// - /// Called from to apply backward-compatibility adjustments - /// to the set of properties produced for this type. Overrides can replace, reorder, or + /// Called from to apply backward-compatibility + /// adjustments to the set of properties produced for this type. Runs after all visitors so + /// adjustments reflect the final state of the library. Overrides can replace, reorder, or /// otherwise rewrite properties based on the . /// /// The properties as produced from the current input spec. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs index b6b2cdd1113..88ff09017da 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs @@ -981,6 +981,7 @@ await MockHelpers.LoadMockGeneratorAsync( var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; Assert.IsNotNull(modelProvider); + modelProvider!.ProcessTypeForBackCompatibility(); var itemsProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Items"); Assert.IsNotNull(itemsProperty); @@ -1016,6 +1017,7 @@ await MockHelpers.LoadMockGeneratorAsync( var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; Assert.IsNotNull(modelProvider); + modelProvider!.ProcessTypeForBackCompatibility(); var elementModelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "ElementModel") as ModelProvider; @@ -1050,6 +1052,7 @@ await MockHelpers.LoadMockGeneratorAsync( var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; Assert.IsNotNull(modelProvider); + modelProvider!.ProcessTypeForBackCompatibility(); var elementEnumProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "ElementEnum") as EnumProvider; @@ -1079,6 +1082,7 @@ await MockHelpers.LoadMockGeneratorAsync( var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; Assert.IsNotNull(modelProvider); + modelProvider!.ProcessTypeForBackCompatibility(); var itemsProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Items"); Assert.IsNotNull(itemsProperty); @@ -1108,6 +1112,7 @@ await MockHelpers.LoadMockGeneratorAsync( var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; Assert.IsNotNull(modelProvider); + modelProvider!.ProcessTypeForBackCompatibility(); var countProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Count"); Assert.IsNotNull(countProperty); @@ -1136,6 +1141,7 @@ await MockHelpers.LoadMockGeneratorAsync( var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; Assert.IsNotNull(modelProvider); + modelProvider!.ProcessTypeForBackCompatibility(); var countProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Count"); Assert.IsNotNull(countProperty); @@ -1167,6 +1173,7 @@ await MockHelpers.LoadMockGeneratorAsync( var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; Assert.IsNotNull(modelProvider); + modelProvider!.ProcessTypeForBackCompatibility(); var statusProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Status"); Assert.IsNotNull(statusProperty); From 2c0f1f7acea2856ff3fc96bf579de6cf15cf1589 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:20:21 +0000 Subject: [PATCH 09/15] Cascade back-compat property type override to cached ParameterProvider Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/9d97c350-132f-41a4-9099-9a728b8be682 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 10 ++++- .../ModelProviders/ModelProviderTests.cs | 40 +++++++++++++++++++ .../MockInputModel.cs | 7 ++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorParameterTypesMatchOverriddenProperty/MockInputModel.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index 19c5e4be86a..799d362a239 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -1277,7 +1277,15 @@ protected internal override IReadOnlyList BuildPropertiesForBa { if (TryGetLastContractPropertyTypeOverride(outputProperty, out var lastContractPropertyType)) { - outputProperty.Type = lastContractPropertyType.ApplyInputSpecProperty(outputProperty.InputProperty); + var newType = lastContractPropertyType.ApplyInputSpecProperty(outputProperty.InputProperty); + outputProperty.Type = newType; + // PropertyProvider.AsParameter is lazily materialized and shared with any constructor + // or method signatures that were built before this back-compat pass runs (e.g. by + // visitors). Cascade the type override onto the cached parameter (and its public + // input variant) so those signatures stay consistent with the overridden property type. + var parameter = outputProperty.AsParameter; + parameter.Update(type: newType); + parameter.ToPublicInputParameter().Update(type: newType.InputType); CodeModelGenerator.Instance.Emitter.Info($"Changed property {Name}.{outputProperty.Name} type to {lastContractPropertyType} to match last contract."); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs index 88ff09017da..2f1fa49b1b7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs @@ -1121,6 +1121,46 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.IsTrue(countProperty!.Type.Equals(new CSharpType(typeof(int), isNullable: true))); } + [Test] + public async Task BackCompat_ConstructorParameterTypesMatchOverriddenProperty() + { + // Regression: constructor parameters are built from PropertyProvider.AsParameter, which + // lazily materializes a ParameterProvider capturing property.Type on first access. Visitors + // that inspect constructors/methods before ProcessTypeForBackCompatibility runs can + // materialize AsParameter with the pre-override type, so when back-compat later rewrites + // property.Type, the cached ctor/method signatures would go out of sync. Verify that the + // back-compat pass cascades the type override onto the shared ParameterProvider. + var inputModel = InputFactory.Model( + "MockInputModel", + properties: + [ + InputFactory.Property("count", InputPrimitiveType.Int32, isRequired: true), + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; + Assert.IsNotNull(modelProvider); + + // Simulate a visitor materializing the constructor (and therefore the property's + // AsParameter) before the back-compat pass runs. + var countProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Count"); + Assert.IsNotNull(countProperty); + _ = countProperty!.AsParameter; + + modelProvider.ProcessTypeForBackCompatibility(); + + // Property type was overridden from int to int? to match the last contract. + var expectedType = new CSharpType(typeof(int), isNullable: true); + Assert.IsTrue(countProperty.Type.Equals(expectedType)); + // The shared ParameterProvider (used by any ctor/method signature built from this + // property) must reflect the overridden type too. + Assert.IsTrue(countProperty.AsParameter.Type.Equals(expectedType)); + Assert.IsTrue(countProperty.AsParameter.ToPublicInputParameter().Type.Equals(expectedType.InputType)); + } + [Test] public async Task BackCompat_ScalarPropertyTypeOverriddenWhenTypeNameDiffers() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorParameterTypesMatchOverriddenProperty/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorParameterTypesMatchOverriddenProperty/MockInputModel.cs new file mode 100644 index 00000000000..1507b180d81 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorParameterTypesMatchOverriddenProperty/MockInputModel.cs @@ -0,0 +1,7 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + public int? Count { get; set; } + } +} From ec6fba2675c6f56f9f2729912ff3e307171064d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:39:21 +0000 Subject: [PATCH 10/15] Simplify cascade-parameter comment Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/14836f9a-718b-442f-aec2-bfa044fc27a6 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index 799d362a239..3f177c64425 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -1279,10 +1279,7 @@ protected internal override IReadOnlyList BuildPropertiesForBa { var newType = lastContractPropertyType.ApplyInputSpecProperty(outputProperty.InputProperty); outputProperty.Type = newType; - // PropertyProvider.AsParameter is lazily materialized and shared with any constructor - // or method signatures that were built before this back-compat pass runs (e.g. by - // visitors). Cascade the type override onto the cached parameter (and its public - // input variant) so those signatures stay consistent with the overridden property type. + // Keep any cached parameters in sync with the overridden property type. var parameter = outputProperty.AsParameter; parameter.Update(type: newType); parameter.ToPublicInputParameter().Update(type: newType.InputType); From d323daf04672499a8c2d0eadb61f26a7dfa9acca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:46:59 +0000 Subject: [PATCH 11/15] Merge origin/main; rename CollectionPropertyTypePreserved to PropertyTypePreserved Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/b5c53470-452b-4a2c-8c32-db0500c917ee Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- ...r-support-sub-exports-2026-3-22-18-8-48.md | 8 + ...um-name-compile-error-2026-3-16-5-51-48.md | 7 + .../fix-playground-line-diff-2026-4-21.md | 7 + .../http-client-java-node-deps-2026-4-23.md | 7 + ...n-addBackApiViewSphinx-2026-3-9-12-35-1.md | 7 + ...-diff-highlight-config-2026-4-23-3-37-0.md | 7 + .github/skills/emitter-prep-for-pr/SKILL.md | 257 ++-------- .github/workflows/copilot-setup-steps.yml | 4 +- .github/workflows/issue-triage.lock.yml | 4 +- .github/workflows/issue-triage.md | 2 +- .github/workflows/python-integration.yml | 10 +- .gitignore | 3 + cspell.yaml | 2 + .../templates/stages/emitter-stages.yml | 72 ++- packages/bundler/src/bundler.ts | 22 +- packages/bundler/test/test.test.ts | 58 ++- packages/http-client-csharp/.dockerignore | 8 + .../emitter/src/code-model-writer.ts | 12 +- .../emitter/src/emit-generate.browser.ts | 61 +++ .../emitter/src/emit-generate.ts | 195 +++++++ .../http-client-csharp/emitter/src/emitter.ts | 214 +------- .../test/Unit/emit-generate-browser.test.ts | 130 +++++ .../emitter/test/Unit/emitter.test.ts | 205 ++------ .../test/Unit/validate-dotnet-sdk.test.ts | 118 +++++ .../eng/pipeline/publish.yml | 4 + .../src/Providers/ClientProvider.cs | 17 +- ...elSerializationExtensionsDefinition.Xml.cs | 2 + .../src/Providers/RestClientProvider.cs | 39 +- .../Providers/ScmMethodProviderCollection.cs | 2 +- .../src/Providers/ScmModelProvider.cs | 13 +- .../src/Snippets/XmlWriterSnippets.cs | 3 + .../XmlAdvancedModel/XmlAdvancedModel.xml | 16 +- ...ValueOverride_CustomTypeDeserialization.cs | 5 +- ...lDeserializationForDiscriminatedSubtype.cs | 5 +- ...serializationHandlesAttributeProperties.cs | 5 +- ...ializationHandlesAttributeWithNamespace.cs | 5 +- ...erializationHandlesElementWithNamespace.cs | 5 +- ...thodBodyContainsPropertyDeserialization.cs | 5 +- ...DeserializationMethodHandlesNestedModel.cs | 5 +- ...hodHandlesOptionalUnwrappedListProperty.cs | 4 +- ...ationMethodHandlesUnwrappedListProperty.cs | 4 +- ...izationMethodHandlesWrappedListProperty.cs | 4 +- .../CanChangePropertyName.cs | 4 +- ...CustomizeAttributeDeserializationMethod.cs | 5 +- .../CanCustomizeDeserializationMethod.cs | 5 +- ...stomizeDeserializationMethodWithOptions.cs | 5 +- .../RestClientProviderTests.cs | 33 ++ .../TestClient.cs | 15 + .../BackCompatibilityChangeCategory.cs | 43 ++ .../src/EmitterRpc/Emitter.cs | 132 ++++- .../src/Expressions/ValueExpression.cs | 3 +- .../src/Providers/ApiVersionEnumProvider.cs | 4 + .../src/Providers/FixedEnumProvider.cs | 28 ++ .../src/Providers/ModelFactoryProvider.cs | 83 ++- .../src/Providers/ModelProvider.cs | 13 +- .../src/Providers/TypeProvider.cs | 11 + .../test/EmitterRpc/EmitterTests.cs | 76 +++ .../ModelFactoryProviderTests.cs | 163 ++++++ .../SampleNamespaceModelFactory.cs | 26 + .../SampleNamespaceModelFactory.cs | 30 ++ .../SampleNamespaceModelFactory.cs | 27 + .../ModelProviders/ModelProviderTests.cs | 27 + .../Internal/ModelSerializationExtensions.cs | 2 + .../Models/XmlAdvancedModel.Serialization.cs | 4 +- .../src/Generated/Models/XmlAdvancedModel.cs | 7 +- .../Generated/Models/XmlItem.Serialization.cs | 4 +- .../src/Generated/Models/XmlItem.cs | 17 - .../XmlModelWithNamespace.Serialization.cs | 4 +- .../Generated/Models/XmlModelWithNamespace.cs | 13 - .../Models/XmlNestedModel.Serialization.cs | 4 +- .../src/Generated/Models/XmlNestedModel.cs | 15 - .../Generated/SampleTypeSpecModelFactory.cs | 9 +- .../Http/Payload/Xml/XmlTests.cs | 12 + .../generator/docs/backward-compatibility.md | 148 ++++++ packages/http-client-csharp/package.json | 5 + .../playground-server/.gitignore | 2 + .../playground-server/Dockerfile | 29 ++ .../playground-server/Program.cs | 263 ++++++++++ .../playground-server.csproj | 10 + .../package.json | 4 +- .../http-client-generator-test/package.json | 4 +- packages/http-client-java/package-lock.json | 475 ++++++++---------- packages/http-client-java/package.json | 20 +- .../http-client-python/emitter/src/emitter.ts | 32 +- .../http-client-python/emitter/src/lib.ts | 7 + .../http-client-python/emitter/src/types.ts | 3 - .../emitter/test/utils.test.ts | 3 + .../eng/scripts/Test-Packages.ps1 | 11 - .../scripts/ci/config/eslint-ci.config.mjs | 10 + .../eng/scripts/ci/dev_requirements.txt | 2 +- .../http-client-python/eng/scripts/ci/lint.ts | 13 +- .../eng/scripts/ci/regenerate.ts | 156 +++++- .../eng/scripts/ci/run-tests.ts | 151 ++++-- .../eng/scripts/ci/run_apiview.py | 38 +- .../eng/scripts/ci/run_mypy.py | 9 +- .../eng/scripts/ci/run_pylint.py | 11 +- .../eng/scripts/ci/run_pyright.py | 7 +- .../eng/scripts/ci/run_sphinx_build.py | 33 +- .../http-client-python/eng/scripts/ci/util.py | 85 +++- .../eng/scripts/setup/run_batch.py | 186 +++++++ .../eng/scripts/setup/venvtools.py | 1 - .../generator/pygen/__init__.py | 1 - .../generator/pygen/codegen/__init__.py | 1 - .../generator/pygen/codegen/models/base.py | 1 - .../pygen/codegen/models/enum_type.py | 1 - .../pygen/codegen/serializers/__init__.py | 11 +- .../codegen/serializers/general_serializer.py | 5 +- .../packaging_templates/pyproject.toml.jinja2 | 2 +- .../http-client-python/generator/setup.py | 2 +- packages/http-client-python/package.json | 2 +- packages/http-client-python/tests/conftest.py | 137 ++--- .../tests/install_packages.py | 146 ++++-- .../tests/mock_api/azure/conftest.py | 32 -- .../tests/mock_api/shared/conftest.py | 36 -- .../tests/mock_api/unbranded/conftest.py | 32 -- .../mock_api/unbranded/test_unbranded.py | 2 +- .../tests/requirements/docs.txt | 2 + .../tests/requirements/lint.txt | 2 +- packages/http-client-python/tests/tox.ini | 70 ++- .../src/react/output-view/file-viewer.tsx | 29 +- .../src/react/output-view/output-view.tsx | 12 +- .../src/react/output-view/use-file-changes.ts | 4 +- packages/playground/src/react/playground.tsx | 35 +- .../playground/src/react/typespec-editor.css | 4 + .../src/react/typespec-editor.module.css | 4 - .../playground/src/react/typespec-editor.tsx | 2 +- pnpm-lock.yaml | 465 +++++++++++------ pnpm-workspace.yaml | 8 +- .../playground-component/playground.tsx | 4 +- .../src/components/react-pages/playground.tsx | 5 +- .../docs/release-notes/typespec-1-11-0.md | 33 -- .../docs/release-notes/typespec-1-11-0.mdx | 210 ++++++++ 132 files changed, 3709 insertions(+), 1696 deletions(-) create mode 100644 .chronus/changes/bundler-support-sub-exports-2026-3-22-18-8-48.md create mode 100644 .chronus/changes/copilot-fix-enum-name-compile-error-2026-3-16-5-51-48.md create mode 100644 .chronus/changes/fix-playground-line-diff-2026-4-21.md create mode 100644 .chronus/changes/http-client-java-node-deps-2026-4-23.md create mode 100644 .chronus/changes/python-addBackApiViewSphinx-2026-3-9-12-35-1.md create mode 100644 .chronus/changes/remove-diff-highlight-config-2026-4-23-3-37-0.md create mode 100644 packages/http-client-csharp/.dockerignore create mode 100644 packages/http-client-csharp/emitter/src/emit-generate.browser.ts create mode 100644 packages/http-client-csharp/emitter/src/emit-generate.ts create mode 100644 packages/http-client-csharp/emitter/test/Unit/emit-generate-browser.test.ts create mode 100644 packages/http-client-csharp/emitter/test/Unit/validate-dotnet-sdk.test.ts create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ParameterNamePreservedFromLastContractView/TestClient.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/BackCompatibilityChangeCategory.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_MultipleParamNamesChanged/SampleNamespaceModelFactory.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_NewPropertyAddedWithRenamedParam/SampleNamespaceModelFactory.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_OnlyParamNameChanged/SampleNamespaceModelFactory.cs create mode 100644 packages/http-client-csharp/playground-server/.gitignore create mode 100644 packages/http-client-csharp/playground-server/Dockerfile create mode 100644 packages/http-client-csharp/playground-server/Program.cs create mode 100644 packages/http-client-csharp/playground-server/playground-server.csproj create mode 100644 packages/http-client-python/eng/scripts/setup/run_batch.py create mode 100644 packages/playground/src/react/typespec-editor.css delete mode 100644 packages/playground/src/react/typespec-editor.module.css delete mode 100644 website/src/content/docs/release-notes/typespec-1-11-0.md create mode 100644 website/src/content/docs/release-notes/typespec-1-11-0.mdx diff --git a/.chronus/changes/bundler-support-sub-exports-2026-3-22-18-8-48.md b/.chronus/changes/bundler-support-sub-exports-2026-3-22-18-8-48.md new file mode 100644 index 00000000000..a68bcda8c03 --- /dev/null +++ b/.chronus/changes/bundler-support-sub-exports-2026-3-22-18-8-48.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/bundler" +--- + +Add support for subpath exports diff --git a/.chronus/changes/copilot-fix-enum-name-compile-error-2026-3-16-5-51-48.md b/.chronus/changes/copilot-fix-enum-name-compile-error-2026-3-16-5-51-48.md new file mode 100644 index 00000000000..6806f26b319 --- /dev/null +++ b/.chronus/changes/copilot-fix-enum-name-compile-error-2026-3-16-5-51-48.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-python" +--- + +Fix enum member names with hyphens generating invalid Python identifiers \ No newline at end of file diff --git a/.chronus/changes/fix-playground-line-diff-2026-4-21.md b/.chronus/changes/fix-playground-line-diff-2026-4-21.md new file mode 100644 index 00000000000..a196e1b390a --- /dev/null +++ b/.chronus/changes/fix-playground-line-diff-2026-4-21.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/playground" +--- + +Fix line-level diff highlighting not appearing in the playground output editor, and reduce typing freezes by coalescing recompilations triggered while a compile is already running. diff --git a/.chronus/changes/http-client-java-node-deps-2026-4-23.md b/.chronus/changes/http-client-java-node-deps-2026-4-23.md new file mode 100644 index 00000000000..c8a4f617b39 --- /dev/null +++ b/.chronus/changes/http-client-java-node-deps-2026-4-23.md @@ -0,0 +1,7 @@ +--- +changeKind: dependencies +packages: + - "@typespec/http-client-java" +--- + +Bump Node.js dependencies for http-client-java and align dependency ranges in test package overrides. diff --git a/.chronus/changes/python-addBackApiViewSphinx-2026-3-9-12-35-1.md b/.chronus/changes/python-addBackApiViewSphinx-2026-3-9-12-35-1.md new file mode 100644 index 00000000000..746bd59aabb --- /dev/null +++ b/.chronus/changes/python-addBackApiViewSphinx-2026-3-9-12-35-1.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/http-client-python" +--- + +Add apiview and sphinx to ci \ No newline at end of file diff --git a/.chronus/changes/remove-diff-highlight-config-2026-4-23-3-37-0.md b/.chronus/changes/remove-diff-highlight-config-2026-4-23-3-37-0.md new file mode 100644 index 00000000000..db2f5466647 --- /dev/null +++ b/.chronus/changes/remove-diff-highlight-config-2026-4-23-3-37-0.md @@ -0,0 +1,7 @@ +--- +changeKind: breaking +packages: + - "@typespec/playground" +--- + +Remove the `newChangeDiff` emitter option. Diff highlighting of changed files and lines is now always enabled in the output viewer. diff --git a/.github/skills/emitter-prep-for-pr/SKILL.md b/.github/skills/emitter-prep-for-pr/SKILL.md index 42a5e1d6339..315fcfbd8a1 100644 --- a/.github/skills/emitter-prep-for-pr/SKILL.md +++ b/.github/skills/emitter-prep-for-pr/SKILL.md @@ -12,7 +12,7 @@ description: > # Emitter Prep for PR Prepares language emitter changes for pull request by running build/format/lint, -creating a changeset with an appropriate message, and pushing to the remote branch. +checking for a changeset, and pushing to the remote branch. **This skill is for language emitter packages only:** @@ -24,116 +24,71 @@ Do NOT use this skill for core TypeSpec packages (compiler, http, openapi3, etc. ## Workflow -### Step 1: Identify changed language emitter packages +### Step 1: Determine the emitter package -Determine which language emitter packages have changes: +Figure out which emitter package the user is working on from context (cwd, recent +changes, or ask). The package will be under `packages//`. -```bash -cd ~/Desktop/github/typespec - -# Compare against upstream/main (microsoft/typespec) if available, otherwise main -BASE_BRANCH=$(git rev-parse --verify upstream/main 2> /dev/null && echo "upstream/main" || echo "main") - -# Filter for language emitter packages only -git diff "$BASE_BRANCH" --name-only | grep "^packages/http-client-" | cut -d'/' -f2 | sort -u -``` - -This filters for `http-client-python`, `http-client-csharp`, `http-client-java`, etc. - -### Step 2: Validate each changed emitter package - -For each changed emitter package (e.g., `http-client-python`, `http-client-csharp`, `http-client-java`): +### Step 2: Build, format, and lint the emitter package ```bash cd ~/Desktop/github/typespec/packages/PACKAGE_NAME # Build npm run build -if [ $? -ne 0 ]; then - echo "Build failed for PACKAGE_NAME" - exit 1 -fi -# Format +# Format (includes both TypeScript and Python formatting) npm run format -if [ $? -ne 0 ]; then - echo "Format failed for PACKAGE_NAME" - exit 1 -fi -# Lint (if available) -npm run lint 2> /dev/null || echo "No lint script for PACKAGE_NAME" +# Lint (emitter-only is fine for quick validation) +npm run lint -- --emitter ``` -If any step fails, report the error and stop. Do not proceed to changeset. - -### Step 3: Run format and spell check at repo root +If any step fails, report the error and stop. Do not proceed. -After validating individual packages, run format and spell check at the repo root: +### Step 3: Run format at repo root ```bash cd ~/Desktop/github/typespec - -# Format all files pnpm format - -# Spell check -pnpm cspell ``` -If spell check fails, either fix the typos or add words to the cspell dictionary. +**Important:** `pnpm format` may touch files outside the emitter package (e.g., +`.devcontainer/`, other packages). When staging changes in Step 6, **only stage +files within the emitter package directory** (`packages/PACKAGE_NAME/`) and +`.chronus/changes/` and `.github/skills/`. Discard any formatting changes to +unrelated files with `git checkout -- `. -### Step 4: Analyze changes for changeset message +### Step 4: Check for existing changeset -Examine the changes to determine an appropriate changeset message: +Check if a changeset already exists for the current branch: ```bash cd ~/Desktop/github/typespec - -# Determine base branch -BASE_BRANCH=$(git rev-parse --verify upstream/main 2> /dev/null && echo "upstream/main" || echo "main") - -# Get commit messages on this branch -git log "$BASE_BRANCH"..HEAD --oneline - -# Get changed files -git diff "$BASE_BRANCH" --name-only - -# Get the actual code changes (for understanding intent) -git diff "$BASE_BRANCH" --stat +BRANCH=$(git rev-parse --abbrev-ref HEAD) +ls .chronus/changes/ | grep -i "$BRANCH" || echo "NO_CHANGESET" ``` -### Step 5: Determine changeset parameters - -Based on the changes, determine: - -1. **changeKind** - one of: - - `internal` - Internal changes not user-facing (tests, docs, refactoring) - - `fix` - Bug fixes (patch version bump) - - `feature` - New features (minor version bump) - - `deprecation` - Deprecating existing features (minor version bump) - - `breaking` - Breaking changes (major version bump) - - `dependencies` - Dependency bumps (patch version bump) +- If a changeset **exists**: Skip to Step 6 (no need to create one). +- If **NO_CHANGESET**: Proceed to Step 5 to create one. -2. **packages** - affected packages, e.g.: - - `@typespec/http-client-python` - - `@typespec/http-client-csharp` - - `@typespec/http-client-java` +### Step 5: Create changeset (only if none exists) -3. **message** - concise description of the change +Ask the user what kind of change this is, or infer from context: -### Step 6: Create changeset file +1. **changeKind** - one of: `internal`, `fix`, `feature`, `deprecation`, `breaking`, `dependencies` +2. **message** - concise user-focused description -Create a changeset file in `.chronus/changes/`: +Then create the file: ```bash cd ~/Desktop/github/typespec - -# Generate filename with timestamp +BRANCH=$(git rev-parse --abbrev-ref HEAD) TIMESTAMP=$(date +"%Y-%m-%d-%H-%M-%S") -FILENAME=".chronus/changes/BRANCH_NAME-${TIMESTAMP}.md" +FILENAME=".chronus/changes/${BRANCH}-${TIMESTAMP}.md" +``` -cat > "$FILENAME" << 'EOF' +```markdown --- changeKind: packages: @@ -141,159 +96,49 @@ packages: --- -EOF -``` - -### Step 7: Show changes and prompt user - -Display all changes to the user: - -```bash -cd ~/Desktop/github/typespec -git status -git diff --stat ``` -Then use AskUserQuestion to confirm: - -- Show the changeset that will be added -- Show the files that will be committed -- Show which remote will be used: "Will push to `origin`" -- Ask: "Do these changes look good to push to origin?" - -Options: - -- "Yes, push to origin" - proceed with commit and push to origin -- "Push to different remote" - ask which remote to use instead -- "Edit changeset" - let user modify the changeset message/kind -- "Cancel" - abort without pushing - -If user selects "Push to different remote", ask which remote name to use and push to that instead of origin. - -### Step 8: Commit and push (if approved) - -If user approves, commit the changes: +### Step 6: Stage, commit, and push ```bash cd ~/Desktop/github/typespec - -# Stage all changes git add -A - -# Commit with descriptive message -git commit -m "$( - cat << 'EOF' - - -Co-Authored-By: Claude Opus 4.5 -EOF -)" +git status ``` -Then push to the user's fork. **Default to `origin`**, but if the user specified a different remote, use that instead: +Show the user what will be committed and ask for confirmation. Then: ```bash -# Get current branch name BRANCH=$(git rev-parse --abbrev-ref HEAD) +git commit -m " -# Push to origin by default (or user-specified remote) -git push -u origin "$BRANCH" -``` - -### Asking about remote - -When prompting the user in Step 7, include the remote that will be used: - -- Show: "Will push to `origin` (your fork)" -- If the user says to use a different remote (e.g., "push to `my_fork`"), use that instead - -**Important:** Never push directly to the `microsoft/typespec` remote (usually named `upstream`). - -## Changeset Message Guidelines - -Write changeset messages that are: +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" -1. **User-focused** - Describe the impact on users, not implementation details -2. **Concise** - One sentence, starting with a verb (Add, Fix, Update, Remove) -3. **Specific** - Mention the feature/fix clearly - -### Examples by changeKind: - -**internal:** - -- "Refactor namespace resolution logic for clarity" -- "Add mock API tests for paging scenarios" -- "Update development tooling and skills" - -**fix:** - -- "Fix incorrect deserialization of nullable enum properties" -- "Fix client initialization when using custom endpoints" - -**feature:** +# Always push to origin (user's fork), never upstream +git push origin "$BRANCH" +``` -- "Add support for XML serialization in request bodies" -- "Add `@clientOption` decorator for customizing client behavior" +## Changeset Guidelines -**deprecation:** +### changeKind reference -- "Deprecate `legacyMode` option in favor of `compatibilityMode`" +- **internal**: Tests, CI/CD, refactoring, docs, skills — not user-facing +- **fix**: Bug fixes users would notice +- **feature**: New user-facing capabilities +- **deprecation**: Marking something as deprecated +- **breaking**: Removing or changing behavior incompatibly +- **dependencies**: Dependency version bumps -**breaking:** +### Message examples -- "Remove deprecated `v1` client generation mode" -- "Change default serialization format from XML to JSON" +- `internal`: "Improve CI pipeline performance and test infrastructure" +- `fix`: "Fix incorrect deserialization of nullable enum properties" +- `feature`: "Add support for XML serialization in request bodies" -## Language Emitter Package Names +### Package names | Folder | Package Name | | -------------------- | ------------------------------ | | `http-client-python` | `@typespec/http-client-python` | | `http-client-csharp` | `@typespec/http-client-csharp` | | `http-client-java` | `@typespec/http-client-java` | - -## Notes - -### When to use each changeKind - -- **internal**: Tests, documentation, refactoring, CI/CD changes, skill updates -- **fix**: Bug fixes that users would notice -- **feature**: New capabilities users can use -- **deprecation**: Marking something as deprecated (still works, but discouraged) -- **breaking**: Removing or changing behavior in incompatible ways - -### Multiple packages - -If changes affect multiple packages **with the same change kind**, list all of them in a single changeset: - -```yaml -packages: - - "@typespec/http-client-python" - - "@typespec/http-client-csharp" -``` - -**If packages have different change kinds, create separate changeset files for each.** For example, if the PR adds a feature to `@typespec/http-client-python` and fixes a bug in `@typespec/http-client-csharp`, create two files: - -```yaml -# File 1: feature for python -changeKind: feature -packages: - - "@typespec/http-client-python" -``` - -```yaml -# File 2: fix for csharp -changeKind: fix -packages: - - "@typespec/http-client-csharp" -``` - -### Skipping changeset - -Some changes don't need a changeset: - -- Changes only to `.github/skills/` (CI will allow this) -- Changes only to test files (if marked in `changedFiles` config) -- Changes only to markdown files (if marked in `changedFiles` config) - -Check `.chronus/config.yaml` for `changedFiles` patterns that are excluded. diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 2516ee22798..80956819b93 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -19,8 +19,8 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install gh-aw extension - uses: github/gh-aw/actions/setup-cli@v0.50.1 + uses: github/gh-aw/actions/setup-cli@v0.68.3 with: version: v0.50.1 diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml index 2b57bed9719..9050b937918 100644 --- a/.github/workflows/issue-triage.lock.yml +++ b/.github/workflows/issue-triage.lock.yml @@ -29,7 +29,7 @@ # # Source: githubnext/agentics/workflows/issue-triage.md@346204513ecfa08b81566450d7d599556807389f # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"e8f92fca0ba8c7c26ae4d31cb42d38344432f98c9b498307d4c1dae4c35c091b","compiler_version":"v0.57.2","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"f3414646cbbee1ca051a87fb8e9788cbac6662cdf37b204a2b76bbb06d263aed","compiler_version":"v0.57.2","strict":true} name: "Agentic Triage" "on": @@ -680,7 +680,7 @@ jobs: "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "issues" + "GITHUB_TOOLSETS": "issues,repos" } }, "safeoutputs": { diff --git a/.github/workflows/issue-triage.md b/.github/workflows/issue-triage.md index a2e5ef7aee4..699c47ad98f 100644 --- a/.github/workflows/issue-triage.md +++ b/.github/workflows/issue-triage.md @@ -31,7 +31,7 @@ safe-outputs: tools: web-fetch: github: - toolsets: [issues] + toolsets: [issues, repos] # If in a public repo, setting `lockdown: false` allows # reading issues, pull requests and comments from 3rd-parties # If in a private repo this has no particular effect. diff --git a/.github/workflows/python-integration.yml b/.github/workflows/python-integration.yml index 4461f39f7c3..bd26838e6e1 100644 --- a/.github/workflows/python-integration.yml +++ b/.github/workflows/python-integration.yml @@ -82,7 +82,7 @@ jobs: venv/bin/python tests/install_packages.py build unbranded tests - name: Upload generated artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: python-generated path: | @@ -119,7 +119,7 @@ jobs: - uses: ./.github/actions/setup-python - name: Download generated artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: python-generated path: . @@ -164,7 +164,7 @@ jobs: - uses: ./.github/actions/setup-python - name: Download generated artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: python-generated path: . @@ -203,7 +203,7 @@ jobs: - uses: ./.github/actions/setup-python - name: Download generated artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: python-generated path: . @@ -250,7 +250,7 @@ jobs: - uses: ./.github/actions/setup-python - name: Download generated artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: python-generated path: . diff --git a/.gitignore b/.gitignore index bd47e87ea2e..c0558a9a41f 100644 --- a/.gitignore +++ b/.gitignore @@ -233,7 +233,10 @@ packages/http-client-python/tests/**/cadl-ranch-coverage.json !packages/http-client-python/package-lock.json packages/http-client-python/micropip.lock packages/http-client-python/venv_build_wheel/ +packages/http-client-python/tests/**.json # http-server-js emitter packages/http-server-js/test/e2e/generated .pnpm-store/ +packages/http-client-python/tests/.uv-cache/ +packages/http-client-python/tests/.wheels/ diff --git a/cspell.yaml b/cspell.yaml index b4f08ccc6b3..00557a34a9c 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -50,6 +50,7 @@ words: - CORGE - createsorreplacesresource - createsorupdatesresource + - Creds - CRUDL - ctxt - dbaeumer @@ -271,6 +272,7 @@ words: - tspwebsitepr - tsvs - typespec + - typespecacr - typespecvs - tzname - Uhoh diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 3e50c505725..2b0a5721387 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -87,6 +87,24 @@ parameters: type: boolean default: false + # The npm script to run to build the emitter for playground bundling. + # Default is "build". C# uses "build:emitter" to skip the .NET generator build. + - name: PlaygroundBundleBuildScript + type: string + default: "build" + + # Path to a Dockerfile (relative to PackagePath) for the playground server. + # When set alongside UploadPlaygroundBundle, the server container is built and + # deployed to Azure App Service after publishing. + - name: PlaygroundServerDockerfile + type: string + default: "" + + # Azure App Service name for the playground server. + - name: PlaygroundServerAppName + type: string + default: "" + stages: # Build stage # Responsible for building the autorest generator and typespec emitter packages @@ -353,10 +371,30 @@ stages: LanguageShortName: ${{ parameters.LanguageShortName }} - ${{ if parameters.UploadPlaygroundBundle }}: + - task: NodeTool@0 + displayName: Use Node 22.x for playground bundle + inputs: + versionSpec: "22.x" - script: npm ci displayName: Install emitter dependencies for playground bundle workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }} - - script: npm run build + - ${{ if parameters.BuildPrereleaseVersion }}: + - script: | + CURRENT_VERSION=$(node -p "require('./package.json').version") + NEW_VERSION="${CURRENT_VERSION}-alpha.${BUILD_BUILDNUMBER}" + echo "Setting version to $NEW_VERSION" + npm version "$NEW_VERSION" --no-git-tag-version + displayName: Stamp build version for playground bundle + workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }} + - ${{ else }}: + - script: | + CURRENT_VERSION=$(node -p "require('./package.json').version") + NEW_VERSION="${CURRENT_VERSION}-beta.${BUILD_BUILDNUMBER}" + echo "Setting version to $NEW_VERSION" + npm version "$NEW_VERSION" --no-git-tag-version + displayName: Stamp build version for playground bundle + workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }} + - script: npm run ${{ parameters.PlaygroundBundleBuildScript }} displayName: Build emitter for playground bundle workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }} - script: npm install -g pnpm @@ -375,6 +413,38 @@ stages: inlineScript: node ./eng/emitters/scripts/upload-bundled-emitter.js ${{ parameters.PackagePath }} workingDirectory: $(Build.SourcesDirectory) + - ${{ if and(parameters.PlaygroundServerDockerfile, parameters.PlaygroundServerAppName) }}: + - task: AzureCLI@1 + displayName: Build and deploy playground server + inputs: + azureSubscription: "Azure SDK Engineering System" + scriptLocation: inlineScript + inlineScript: | + set -e + REGISTRY="typespecacr" + RESOURCE_GROUP="typespec" + APP_NAME="${{ parameters.PlaygroundServerAppName }}" + IMAGE="$REGISTRY.azurecr.io/$APP_NAME:$(Build.BuildId)" + + # Build and push Docker image to ACR + echo "Building Docker image: $IMAGE" + CONTEXT="$(Build.SourcesDirectory)/${{ parameters.PackagePath }}" + az acr build \ + --registry "$REGISTRY" \ + --image "$APP_NAME:$(Build.BuildId)" \ + --file "$CONTEXT/${{ parameters.PlaygroundServerDockerfile }}" \ + "$CONTEXT" + + # Update App Service container image + az webapp config container set \ + --name "$APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --container-image-name "$IMAGE" \ + --container-registry-url "https://$REGISTRY.azurecr.io" + + echo "Deployed to https://$APP_NAME.azurewebsites.net" + workingDirectory: $(Build.SourcesDirectory) + templateContext: outputs: - output: pipelineArtifact diff --git a/packages/bundler/src/bundler.ts b/packages/bundler/src/bundler.ts index 8e8676c1c1d..3a62a7129b6 100644 --- a/packages/bundler/src/bundler.ts +++ b/packages/bundler/src/bundler.ts @@ -26,6 +26,7 @@ export interface ExportData { default?: string; import?: string; types?: string; + typespec?: string; } export interface TypeSpecBundle { @@ -60,7 +61,7 @@ interface PackageJson { tspMain?: string; peerDependencies: string[]; dependencies: string[]; - exports?: Record; + exports?: Record; } export interface CreateTypeSpecBundleOptions { @@ -183,6 +184,25 @@ async function createEsBuildContext( typespecFiles[filename] = sourceFile.file.text; } + // Also compile sub-exports with typespec entry points to include their source files + for (const [, value] of Object.entries(definition.exports)) { + const typespecEntry = typeof value === "object" ? value.typespec : undefined; + if (typespecEntry) { + const subEntryPoint = resolvePath(libraryPath, typespecEntry); + const subProgram = await compile(NodeHost, subEntryPoint, { + noEmit: true, + }); + for (const file of subProgram.jsSourceFiles.keys()) { + if (file.startsWith(libraryPath)) { + jsFiles.add(file); + } + } + for (const [filename, sourceFile] of subProgram.sourceFiles) { + typespecFiles[filename] = sourceFile.file.text; + } + } + } + const content = createBundleEntrypoint({ libraryPath, mainFile: definition.main, diff --git a/packages/bundler/test/test.test.ts b/packages/bundler/test/test.test.ts index 0b5d7b5ba85..20ae2f444bb 100644 --- a/packages/bundler/test/test.test.ts +++ b/packages/bundler/test/test.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, rm, writeFile } from "fs/promises"; +import { mkdir, mkdtemp, rm, writeFile } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; import { describe, expect, it } from "vitest"; @@ -51,4 +51,60 @@ describe("bundler", () => { await rm(tmpDir, { recursive: true }); } }); + + it("includes typespec source files from sub-exports", async () => { + const tmpDir = await mkdtemp(join(tmpdir(), "typespec-bundler-test-")); + try { + await mkdir(join(tmpDir, "lib", "sub"), { recursive: true }); + + await writeFile( + join(tmpDir, "package.json"), + JSON.stringify({ + name: "test-lib", + version: "1.0.0", + main: "index.js", + tspMain: "lib/main.tsp", + peerDependencies: {}, + exports: { + ".": { + typespec: "./lib/main.tsp", + default: "./index.js", + }, + "./sub": { + typespec: "./lib/sub/main.tsp", + default: "./sub.js", + }, + }, + }), + ); + await writeFile( + join(tmpDir, "lib", "main.tsp"), + ['import "../index.js";', "namespace TestLib;"].join("\n"), + ); + await writeFile( + join(tmpDir, "lib", "sub", "main.tsp"), + [ + 'import "../../index.js";', + "namespace TestLib.Sub;", + "model SubModel { x: string; }", + ].join("\n"), + ); + await writeFile(join(tmpDir, "index.js"), "export function $myDec(context, target) { }"); + await writeFile(join(tmpDir, "sub.js"), "export const subExport = true;"); + + const bundle = await createTypeSpecBundle(tmpDir, { minify: false }); + const indexFile = bundle.files.find((f) => f.filename === "index.js"); + expect(indexFile).toBeDefined(); + + // The main bundle should include the sub-export's typespec source files + expect(indexFile!.content).toContain("lib/sub/main.tsp"); + expect(indexFile!.content).toContain("SubModel"); + + // The sub-export JS entry should also be bundled + const subFile = bundle.files.find((f) => f.filename === "sub.js"); + expect(subFile, "sub.js should be in bundle output").toBeDefined(); + } finally { + await rm(tmpDir, { recursive: true }); + } + }); }); diff --git a/packages/http-client-csharp/.dockerignore b/packages/http-client-csharp/.dockerignore new file mode 100644 index 00000000000..e6f62b1d693 --- /dev/null +++ b/packages/http-client-csharp/.dockerignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +emitter/ +.tspd/ +**/artifacts/ +*.md +*.tsp +package-lock.json diff --git a/packages/http-client-csharp/emitter/src/code-model-writer.ts b/packages/http-client-csharp/emitter/src/code-model-writer.ts index 994d0bad2e7..e60360c32b5 100644 --- a/packages/http-client-csharp/emitter/src/code-model-writer.ts +++ b/packages/http-client-csharp/emitter/src/code-model-writer.ts @@ -8,6 +8,16 @@ import { CSharpEmitterContext } from "./sdk-context.js"; import { CodeModel } from "./type/code-model.js"; import { Configuration } from "./type/configuration.js"; +/** + * Serializes the code model to a JSON string with reference tracking. + * @param context - The CSharp emitter context + * @param codeModel - The code model to serialize + * @beta + */ +export function serializeCodeModel(context: CSharpEmitterContext, codeModel: CodeModel): string { + return prettierOutput(JSON.stringify(buildJson(context, codeModel), transformJSONProperties, 2)); +} + /** * Writes the code model to the output folder. Should only be used by autorest.csharp. * @param context - The CSharp emitter context @@ -22,7 +32,7 @@ export async function writeCodeModel( ) { await context.program.host.writeFile( resolvePath(outputFolder, tspOutputFileName), - prettierOutput(JSON.stringify(buildJson(context, codeModel), transformJSONProperties, 2)), + serializeCodeModel(context, codeModel), ); } diff --git a/packages/http-client-csharp/emitter/src/emit-generate.browser.ts b/packages/http-client-csharp/emitter/src/emit-generate.browser.ts new file mode 100644 index 00000000000..cb557a2ba6d --- /dev/null +++ b/packages/http-client-csharp/emitter/src/emit-generate.browser.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +// Browser implementation: sends code model to a playground server for generation. + +import { resolvePath } from "@typespec/compiler"; +import type { GenerateOptions } from "./emit-generate.js"; +import { CSharpEmitterContext } from "./sdk-context.js"; + +const SERVER_URL = "https://csharp-playground-server.azurewebsites.net"; +const MAX_RESPONSE_SIZE = 10 * 1024 * 1024; // 10 MB + +export async function generate( + sdkContext: CSharpEmitterContext, + codeModelJson: string, + configJson: string, + options: GenerateOptions, +): Promise { + const response = await fetch(`${SERVER_URL}/generate`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + codeModel: codeModelJson, + configuration: configJson, + generatorName: options.generatorName, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Playground server error (${response.status}): ${errorText}`); + } + + const contentType = response.headers.get("content-type"); + if (!contentType?.includes("application/json")) { + throw new Error(`Unexpected response content-type: ${contentType}`); + } + + const contentLength = response.headers.get("content-length"); + if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) { + throw new Error( + `Response too large: ${contentLength} bytes exceeds ${MAX_RESPONSE_SIZE} byte limit`, + ); + } + + const result = await response.json(); + + if (!result || !Array.isArray(result.files)) { + throw new Error("Invalid response: expected { files: [...] }"); + } + + for (const file of result.files) { + if (typeof file.path !== "string" || typeof file.content !== "string") { + throw new Error(`Invalid file entry: expected { path: string, content: string }`); + } + await sdkContext.program.host.writeFile( + resolvePath(options.outputFolder, file.path), + file.content, + ); + } +} diff --git a/packages/http-client-csharp/emitter/src/emit-generate.ts b/packages/http-client-csharp/emitter/src/emit-generate.ts new file mode 100644 index 00000000000..41d14546b9b --- /dev/null +++ b/packages/http-client-csharp/emitter/src/emit-generate.ts @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +// Node.js implementation: runs the .NET generator locally via subprocess. + +import { + createDiagnosticCollector, + Diagnostic, + getDirectoryPath, + joinPaths, + NoTarget, + resolvePath, +} from "@typespec/compiler"; +import fs from "fs"; +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { + _minSupportedDotNetSdkVersion, + configurationFileName, + tspOutputFileName, +} from "./constants.js"; +import { createDiagnostic } from "./lib/lib.js"; +import { execAsync, execCSharpGenerator } from "./lib/utils.js"; +import { CSharpEmitterContext } from "./sdk-context.js"; + +export interface GenerateOptions { + outputFolder: string; + packageName: string; + generatorName: string; + newProject: boolean; + debug: boolean; + saveInputs: boolean; + emitterExtensionPath?: string; +} + +function findProjectRoot(path: string): string | undefined { + let current = path; + while (true) { + const pkgPath = joinPaths(current, "package.json"); + try { + if (fs.statSync(pkgPath)?.isFile()) { + return current; + } + } catch { + // file doesn't exist + } + const parent = getDirectoryPath(current); + if (parent === current) { + return undefined; + } + current = parent; + } +} + +function checkFile(pkgPath: string) { + try { + return fs.statSync(pkgPath); + } catch { + return undefined; + } +} + +export async function generate( + sdkContext: CSharpEmitterContext, + codeModelJson: string, + configJson: string, + options: GenerateOptions, +): Promise { + const diagnostics = createDiagnosticCollector(); + + const generatedFolder = resolvePath(options.outputFolder, "src", "Generated"); + if (!fs.existsSync(generatedFolder)) { + fs.mkdirSync(generatedFolder, { recursive: true }); + } + + // Write code model and configuration to disk for the generator + await sdkContext.program.host.writeFile( + resolvePath(options.outputFolder, tspOutputFileName), + codeModelJson, + ); + await sdkContext.program.host.writeFile( + resolvePath(options.outputFolder, configurationFileName), + configJson, + ); + + const csProjFile = resolvePath(options.outputFolder, "src", `${options.packageName}.csproj`); + + const emitterPath = options.emitterExtensionPath ?? import.meta.url; + const projectRoot = findProjectRoot(dirname(fileURLToPath(emitterPath))); + const generatorPath = resolvePath( + projectRoot + "/dist/generator/Microsoft.TypeSpec.Generator.dll", + ); + + try { + const result = await execCSharpGenerator(sdkContext, { + generatorPath: generatorPath, + outputFolder: options.outputFolder, + generatorName: options.generatorName, + newProject: options.newProject || !checkFile(csProjFile), + debug: options.debug, + }); + if (result.exitCode !== 0) { + const isValid = diagnostics.pipe( + await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion), + ); + if (isValid) { + throw new Error( + `Failed to generate the library. Exit code: ${result.exitCode}.\nStackTrace: \n${result.stderr}`, + ); + } + } + } catch (error: any) { + const isValid = diagnostics.pipe( + await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion), + ); + if (isValid) throw new Error(error, { cause: error }); + } + + if (!options.saveInputs) { + sdkContext.program.host.rm(resolvePath(options.outputFolder, tspOutputFileName)); + sdkContext.program.host.rm(resolvePath(options.outputFolder, configurationFileName)); + } + + sdkContext.program.reportDiagnostics(diagnostics.diagnostics); +} + +/** @internal */ +export async function _validateDotNetSdk( + sdkContext: CSharpEmitterContext, + minMajorVersion: number, +): Promise<[boolean, readonly Diagnostic[]]> { + const diagnostics = createDiagnosticCollector(); + try { + const result = await execAsync("dotnet", ["--version"], { stdio: "pipe" }); + return diagnostics.wrap( + diagnostics.pipe(validateDotNetSdkVersionCore(result.stdout, minMajorVersion)), + ); + } catch (error: any) { + if (error && "code" in error && error["code"] === "ENOENT") { + diagnostics.add( + createDiagnostic({ + code: "invalid-dotnet-sdk-dependency", + messageId: "missing", + format: { + dotnetMajorVersion: `${minMajorVersion}`, + downloadUrl: "https://dotnet.microsoft.com/", + }, + target: NoTarget, + }), + ); + } + return diagnostics.wrap(false); + } +} + +function validateDotNetSdkVersionCore( + version: string, + minMajorVersion: number, +): [boolean, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + if (version) { + const dotIndex = version.indexOf("."); + const firstPart = dotIndex === -1 ? version : version.substring(0, dotIndex); + const major = Number(firstPart); + + if (isNaN(major)) { + return diagnostics.wrap(false); + } + if (major < minMajorVersion) { + diagnostics.add( + createDiagnostic({ + code: "invalid-dotnet-sdk-dependency", + messageId: "invalidVersion", + format: { + installedVersion: version, + dotnetMajorVersion: `${minMajorVersion}`, + downloadUrl: "https://dotnet.microsoft.com/", + }, + target: NoTarget, + }), + ); + return diagnostics.wrap(false); + } + return diagnostics.wrap(true); + } else { + diagnostics.add( + createDiagnostic({ + code: "general-error", + format: { message: "Cannot get the installed .NET SDK version." }, + target: NoTarget, + }), + ); + return diagnostics.wrap(false); + } +} diff --git a/packages/http-client-csharp/emitter/src/emitter.ts b/packages/http-client-csharp/emitter/src/emitter.ts index a41e196e09f..a24e86119e7 100644 --- a/packages/http-client-csharp/emitter/src/emitter.ts +++ b/packages/http-client-csharp/emitter/src/emitter.ts @@ -2,55 +2,18 @@ // Licensed under the MIT License. See License.txt in the project root for license information. import { createSdkContext, SdkContext } from "@azure-tools/typespec-client-generator-core"; -import { - createDiagnosticCollector, - Diagnostic, - EmitContext, - getDirectoryPath, - joinPaths, - NoTarget, - Program, - resolvePath, -} from "@typespec/compiler"; -import fs, { statSync } from "fs"; -import { dirname, resolve } from "path"; -import { fileURLToPath } from "url"; -import { writeCodeModel, writeConfiguration } from "./code-model-writer.js"; -import { - _minSupportedDotNetSdkVersion, - configurationFileName, - tspOutputFileName, -} from "./constants.js"; +import { createDiagnosticCollector, Diagnostic, EmitContext, Program } from "@typespec/compiler"; +import { resolve } from "path"; +import { serializeCodeModel } from "./code-model-writer.js"; +import { generate } from "./emit-generate.js"; import { createModel } from "./lib/client-model-builder.js"; -import { createDiagnostic } from "./lib/lib.js"; import { LoggerLevel } from "./lib/logger-level.js"; import { Logger } from "./lib/logger.js"; -import { execAsync, execCSharpGenerator } from "./lib/utils.js"; import { CSharpEmitterOptions, resolveOptions } from "./options.js"; import { createCSharpEmitterContext, CSharpEmitterContext } from "./sdk-context.js"; import { CodeModel } from "./type/code-model.js"; import { Configuration } from "./type/configuration.js"; -/** - * Look for the project root by looking up until a `package.json` is found. - * @param path Path to start looking - */ -function findProjectRoot(path: string): string | undefined { - let current = path; - while (true) { - const pkgPath = joinPaths(current, "package.json"); - const stats = checkFile(pkgPath); - if (stats?.isFile()) { - return current; - } - const parent = getDirectoryPath(current); - if (parent === current) { - return undefined; - } - current = parent; - } -} - /** * Creates a code model by executing the full emission logic. * This function can be called by downstream emitters to generate a code model and collect diagnostics. @@ -112,81 +75,25 @@ export async function emitCodeModel( // Apply optional code model update callback const updatedRoot = updateCodeModel ? updateCodeModel(root, sdkContext) : root; - const generatedFolder = resolvePath(outputFolder, "src", "Generated"); - - if (!fs.existsSync(generatedFolder)) { - fs.mkdirSync(generatedFolder, { recursive: true }); - } - - // emit tspCodeModel.json - await writeCodeModel(sdkContext, updatedRoot, outputFolder); - const namespace = updatedRoot.name; const configurations: Configuration = createConfiguration(options, namespace, sdkContext); - //emit configuration.json - await writeConfiguration(sdkContext, configurations, outputFolder); + // Serialize code model and configuration + const codeModelJson = serializeCodeModel(sdkContext, updatedRoot); + const configJson = JSON.stringify(configurations, null, 2) + "\n"; - const csProjFile = resolvePath( + // Generate C# code via platform-specific implementation. + // In Node.js this runs the .NET generator locally. + // In the browser this sends the code model to a playground server. + await generate(sdkContext, codeModelJson, configJson, { outputFolder, - "src", - `${configurations["package-name"]}.csproj`, - ); - logger.info(`Checking if ${csProjFile} exists`); - - const emitterPath = options["emitter-extension-path"] ?? import.meta.url; - const projectRoot = findProjectRoot(dirname(fileURLToPath(emitterPath))); - const generatorPath = resolvePath( - projectRoot + "/dist/generator/Microsoft.TypeSpec.Generator.dll", - ); - - try { - const result = await execCSharpGenerator(sdkContext, { - generatorPath: generatorPath, - outputFolder: outputFolder, - generatorName: options["generator-name"], - newProject: options["new-project"] || !checkFile(csProjFile), - debug: options.debug ?? false, - }); - if (result.exitCode !== 0) { - const isValid = diagnostics.pipe( - await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion), - ); - // if the dotnet sdk is valid, the error is not dependency issue, log it as normal - if (isValid) { - diagnostics.add( - createDiagnostic({ - code: "general-error", - format: { - message: `Failed to generate the library. Exit code: ${result.exitCode}.\n${result.stderr}`, - }, - target: NoTarget, - }), - ); - } - } - } catch (error: any) { - const isValid = diagnostics.pipe( - await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion), - ); - // if the dotnet sdk is valid, the error is not dependency issue, log it as normal - if (isValid) { - diagnostics.add( - createDiagnostic({ - code: "general-error", - format: { - message: `Failed to generate the library. Error: ${error.message ?? error}`, - }, - target: NoTarget, - }), - ); - } - } - if (!options["save-inputs"]) { - // delete - context.program.host.rm(resolvePath(outputFolder, tspOutputFileName)); - context.program.host.rm(resolvePath(outputFolder, configurationFileName)); - } + packageName: configurations["package-name"] ?? "", + generatorName: options["generator-name"], + newProject: options["new-project"], + debug: options.debug ?? false, + saveInputs: options["save-inputs"] ?? false, + emitterExtensionPath: options["emitter-extension-path"], + }); } } @@ -234,88 +141,3 @@ export function createConfiguration( license: sdkContext.sdkPackage.licenseInfo, }; } - -/** check the dotnet sdk installation. - * Report diagnostic if dotnet sdk is not installed or its version does not meet prerequisite - * @param sdkContext - The SDK context - * @param minVersionRequisite - The minimum required major version - * @returns A tuple containing whether the SDK is valid and any diagnostics - * @internal - */ -export async function _validateDotNetSdk( - sdkContext: CSharpEmitterContext, - minMajorVersion: number, -): Promise<[boolean, readonly Diagnostic[]]> { - const diagnostics = createDiagnosticCollector(); - try { - const result = await execAsync("dotnet", ["--version"], { stdio: "pipe" }); - return diagnostics.wrap( - diagnostics.pipe(validateDotNetSdkVersionCore(sdkContext, result.stdout, minMajorVersion)), - ); - } catch (error: any) { - if (error && "code" in error && error["code"] === "ENOENT") { - diagnostics.add( - createDiagnostic({ - code: "invalid-dotnet-sdk-dependency", - messageId: "missing", - format: { - dotnetMajorVersion: `${minMajorVersion}`, - downloadUrl: "https://dotnet.microsoft.com/", - }, - target: NoTarget, - }), - ); - } - return diagnostics.wrap(false); - } -} - -function validateDotNetSdkVersionCore( - sdkContext: CSharpEmitterContext, - version: string, - minMajorVersion: number, -): [boolean, readonly Diagnostic[]] { - const diagnostics = createDiagnosticCollector(); - if (version) { - const dotIndex = version.indexOf("."); - const firstPart = dotIndex === -1 ? version : version.substring(0, dotIndex); - const major = Number(firstPart); - - if (isNaN(major)) { - return diagnostics.wrap(false); - } - if (major < minMajorVersion) { - diagnostics.add( - createDiagnostic({ - code: "invalid-dotnet-sdk-dependency", - messageId: "invalidVersion", - format: { - installedVersion: version, - dotnetMajorVersion: `${minMajorVersion}`, - downloadUrl: "https://dotnet.microsoft.com/", - }, - target: NoTarget, - }), - ); - return diagnostics.wrap(false); - } - return diagnostics.wrap(true); - } else { - diagnostics.add( - createDiagnostic({ - code: "general-error", - format: { message: "Cannot get the installed .NET SDK version." }, - target: NoTarget, - }), - ); - return diagnostics.wrap(false); - } -} - -function checkFile(pkgPath: string) { - try { - return statSync(pkgPath); - } catch (error) { - return undefined; - } -} diff --git a/packages/http-client-csharp/emitter/test/Unit/emit-generate-browser.test.ts b/packages/http-client-csharp/emitter/test/Unit/emit-generate-browser.test.ts new file mode 100644 index 00000000000..9d47f0c06b8 --- /dev/null +++ b/packages/http-client-csharp/emitter/test/Unit/emit-generate-browser.test.ts @@ -0,0 +1,130 @@ +vi.resetModules(); + +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { GenerateOptions } from "../../src/emit-generate.js"; +import type { CSharpEmitterContext } from "../../src/sdk-context.js"; + +// Mock @typespec/compiler to provide resolvePath +vi.mock("@typespec/compiler", () => ({ + resolvePath: (...segments: string[]) => segments.join("/"), +})); + +// Create a mock context with a writable host +function createMockContext(): CSharpEmitterContext { + return { + program: { + host: { + writeFile: vi.fn(), + }, + }, + } as unknown as CSharpEmitterContext; +} + +const defaultOptions: GenerateOptions = { + outputFolder: "/output", + generatorName: "ScmCodeModelGenerator", + packageName: "TestPackage", + newProject: false, + debug: false, + saveInputs: false, +}; + +function createMockResponse(body: any, opts?: { ok?: boolean; status?: number }) { + return { + ok: opts?.ok ?? true, + status: opts?.status ?? 200, + headers: new Headers({ "content-type": "application/json" }), + json: vi.fn().mockResolvedValue(body), + text: vi.fn().mockResolvedValue(JSON.stringify(body)), + }; +} + +describe("emit-generate.browser", () => { + let generate: typeof import("../../src/emit-generate.browser.js").generate; + + beforeEach(async () => { + vi.resetModules(); + generate = (await import("../../src/emit-generate.browser.js")).generate; + }); + + it("should POST to the server URL", async () => { + global.fetch = vi.fn().mockResolvedValue(createMockResponse({ files: [] })); + + const ctx = createMockContext(); + await generate(ctx, '{"model":"test"}', '{"config":"test"}', defaultOptions); + + expect(fetch).toHaveBeenCalledWith( + "https://csharp-playground-server.azurewebsites.net/generate", + expect.objectContaining({ + method: "POST", + headers: { "Content-Type": "application/json" }, + }), + ); + }); + + it("should send codeModel, configuration, and generatorName in request body", async () => { + global.fetch = vi.fn().mockResolvedValue(createMockResponse({ files: [] })); + + const ctx = createMockContext(); + await generate(ctx, '{"model":"data"}', '{"namespace":"Test"}', { + ...defaultOptions, + generatorName: "CustomGenerator", + }); + + const callArgs = vi.mocked(fetch).mock.calls[0]; + const body = JSON.parse(callArgs[1]!.body as string); + expect(body).toEqual({ + codeModel: '{"model":"data"}', + configuration: '{"namespace":"Test"}', + generatorName: "CustomGenerator", + }); + }); + + it("should write response files to the host", async () => { + global.fetch = vi.fn().mockResolvedValue( + createMockResponse({ + files: [ + { path: "src/Generated/Model.cs", content: "public class Model {}" }, + { path: "src/Generated/Client.cs", content: "public class Client {}" }, + ], + }), + ); + + const ctx = createMockContext(); + await generate(ctx, '{"model":"test"}', '{"config":"test"}', defaultOptions); + + const writeFile = vi.mocked(ctx.program.host.writeFile); + expect(writeFile).toHaveBeenCalledTimes(2); + expect(writeFile).toHaveBeenCalledWith( + "/output/src/Generated/Model.cs", + "public class Model {}", + ); + expect(writeFile).toHaveBeenCalledWith( + "/output/src/Generated/Client.cs", + "public class Client {}", + ); + }); + + it("should throw on non-OK response", async () => { + global.fetch = vi + .fn() + .mockResolvedValue( + createMockResponse({ error: "Generator failed" }, { ok: false, status: 500 }), + ); + + const ctx = createMockContext(); + await expect( + generate(ctx, '{"model":"test"}', '{"config":"test"}', defaultOptions), + ).rejects.toThrow("Playground server error (500)"); + }); + + it("should handle empty files array in response", async () => { + global.fetch = vi.fn().mockResolvedValue(createMockResponse({ files: [] })); + + const ctx = createMockContext(); + await generate(ctx, '{"model":"test"}', '{"config":"test"}', defaultOptions); + + const writeFile = vi.mocked(ctx.program.host.writeFile); + expect(writeFile).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts b/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts index 640d242a63e..c937a9ea0d4 100644 --- a/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts @@ -1,15 +1,13 @@ vi.resetModules(); -import { Diagnostic, EmitContext, Program } from "@typespec/compiler"; +import { EmitContext, Program } from "@typespec/compiler"; import { TestHost } from "@typespec/compiler/testing"; -import { strictEqual } from "assert"; -import { statSync } from "fs"; -import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { generate } from "../../src/emit-generate.js"; import { execAsync, execCSharpGenerator } from "../../src/lib/utils.js"; import { CSharpEmitterOptions } from "../../src/options.js"; import { CodeModel } from "../../src/type/code-model.js"; import { - createCSharpSdkContext, createEmitterContext, createEmitterTestHost, getCreateSdkContext, @@ -61,6 +59,10 @@ describe("$onEmit tests", () => { execAsync: vi.fn(), })); + vi.mock("../../src/emit-generate.js", () => ({ + generate: vi.fn(), + })); + vi.mock("../../src/lib/client-model-builder.js", () => ({ createModel: vi.fn().mockReturnValue([{ name: "TestNamespace" }, []]), })); @@ -139,68 +141,53 @@ describe("$onEmit tests", () => { ); }); - it("should set newProject to TRUE if .csproj file DOES NOT exist", async () => { - vi.mocked(statSync).mockImplementation(() => { - throw new Error("File not found"); - }); - - const context: EmitContext = createEmitterContext(program); - await $onEmit(context); - - expect(execCSharpGenerator).toHaveBeenCalledWith(expect.anything(), { - generatorPath: expect.any(String), - outputFolder: undefined, - generatorName: "ScmCodeModelGenerator", - newProject: true, // Ensure this is passed as true - debug: false, - }); - }); - - it("should set newProject to FALSE if .csproj file DOES exist", async () => { - vi.mocked(statSync).mockReturnValue({ isFile: () => true } as any); - + it("should pass newProject FALSE by default", async () => { const context: EmitContext = createEmitterContext(program); await $onEmit(context); - expect(execCSharpGenerator).toHaveBeenCalledWith(expect.anything(), { - generatorPath: expect.any(String), - outputFolder: undefined, - generatorName: "ScmCodeModelGenerator", - newProject: false, // Ensure this is passed as false - debug: false, - }); + expect(generate).toHaveBeenCalledWith( + expect.anything(), + expect.any(String), + expect.any(String), + expect.objectContaining({ + newProject: false, + generatorName: "ScmCodeModelGenerator", + }), + ); }); - it("should set newProject to TRUE if passed in options", async () => { - vi.mocked(statSync).mockReturnValue({ isFile: () => true } as any); - + it("should pass newProject TRUE when set in options", async () => { const context: EmitContext = createEmitterContext(program, { "new-project": true, }); await $onEmit(context); - expect(execCSharpGenerator).toHaveBeenCalledWith(expect.anything(), { - generatorPath: expect.any(String), - outputFolder: undefined, - generatorName: "ScmCodeModelGenerator", - newProject: true, // Ensure this is passed as true - debug: false, - }); - }); - it("should set newProject to FALSE if passed in options", async () => { - vi.mocked(statSync).mockReturnValue({ isFile: () => true } as any); + expect(generate).toHaveBeenCalledWith( + expect.anything(), + expect.any(String), + expect.any(String), + expect.objectContaining({ + newProject: true, + generatorName: "ScmCodeModelGenerator", + }), + ); + }); + it("should pass newProject FALSE when set in options", async () => { const context: EmitContext = createEmitterContext(program, { "new-project": false, }); await $onEmit(context); - expect(execCSharpGenerator).toHaveBeenCalledWith(expect.anything(), { - generatorPath: expect.any(String), - outputFolder: undefined, - generatorName: "ScmCodeModelGenerator", - newProject: false, // Ensure this is passed as true - debug: false, - }); + + expect(generate).toHaveBeenCalledWith( + expect.anything(), + expect.any(String), + expect.any(String), + expect.objectContaining({ + newProject: false, + generatorName: "ScmCodeModelGenerator", + }), + ); }); }); @@ -256,117 +243,3 @@ describe("emitCodeModel tests", () => { expect(Array.isArray(diagnostics)).toBe(true); }); }); - -describe("Test _validateDotNetSdk", () => { - let runner: TestHost; - let program: Program; - const minVersion = 8; - let _validateDotNetSdk: (arg0: any, arg1: number) => Promise<[boolean, readonly Diagnostic[]]>; - - beforeEach(async () => { - vi.resetModules(); - runner = await createEmitterTestHost(); - program = await typeSpecCompile( - ` - op test( - @query - @encode(DurationKnownEncoding.ISO8601) - input: duration - ): NoContentResponse; - `, - runner, - ); - // Restore all mocks before each test - vi.restoreAllMocks(); - vi.mock("../../src/lib/utils.js", () => ({ - execCSharpGenerator: vi.fn(), - execAsync: vi.fn(), - })); - - // dynamically import the module to get the $onEmit function - // we avoid importing it at the top to allow mocking of dependencies - _validateDotNetSdk = (await import("../../src/emitter.js"))._validateDotNetSdk; - }); - - it("should return false and report diagnostic when dotnet SDK is not installed.", async () => { - /* mock the scenario that dotnet SDK is not installed, so execAsync will throw exception with error ENOENT */ - const error: any = new Error("ENOENT: no such file or directory"); - error.code = "ENOENT"; - (execAsync as Mock).mockRejectedValueOnce(error); - const context = createEmitterContext(program); - const sdkContext = await createCSharpSdkContext(context); - const [result, diagnostics] = await _validateDotNetSdk(sdkContext, minVersion); - // Report collected diagnostics to program - program.reportDiagnostics(diagnostics); - expect(result).toBe(false); - strictEqual(program.diagnostics.length, 1); - strictEqual( - program.diagnostics[0].code, - "@typespec/http-client-csharp/invalid-dotnet-sdk-dependency", - ); - strictEqual( - program.diagnostics[0].message, - "The dotnet command was not found in the PATH. Please install the .NET SDK version 8 or above. Guidance for installing the .NET SDK can be found at https://dotnet.microsoft.com/.", - ); - }); - - it("should return true for installed SDK version whose major equals min supported version", async () => { - /* mock the scenario that the installed SDK version whose major equals min supported version */ - (execAsync as Mock).mockResolvedValueOnce({ - exitCode: 0, - stdio: "", - stdout: "8.0.204", - stderr: "", - proc: { pid: 0, output: "", stdout: "", stderr: "", stdin: "" }, - }); - const context = createEmitterContext(program); - const sdkContext = await createCSharpSdkContext(context); - const [result, diagnostics] = await _validateDotNetSdk(sdkContext, minVersion); - expect(result).toBe(true); - /* no diagnostics */ - strictEqual(diagnostics.length, 0); - }); - - it("should return true for installed SDK version whose major greaters than min supported version", async () => { - /* mock the scenario that the installed SDK version whose major greater than min supported version */ - (execAsync as Mock).mockResolvedValueOnce({ - exitCode: 0, - stdio: "", - stdout: "9.0.102", - stderr: "", - proc: { pid: 0, output: "", stdout: "", stderr: "", stdin: "" }, - }); - const context = createEmitterContext(program); - const sdkContext = await createCSharpSdkContext(context); - const [result, diagnostics] = await _validateDotNetSdk(sdkContext, minVersion); - expect(result).toBe(true); - /* no diagnostics */ - strictEqual(diagnostics.length, 0); - }); - - it("should return false and report diagnostic for invalid .NET SDK version", async () => { - /* mock the scenario that the installed SDK version whose major less than min supported version */ - (execAsync as Mock).mockResolvedValueOnce({ - exitCode: 0, - stdio: "", - stdout: "5.0.408", - stderr: "", - proc: { pid: 0, output: "", stdout: "", stderr: "", stdin: "" }, - }); - const context = createEmitterContext(program); - const sdkContext = await createCSharpSdkContext(context); - const [result, diagnostics] = await _validateDotNetSdk(sdkContext, minVersion); - // Report collected diagnostics to program - program.reportDiagnostics(diagnostics); - expect(result).toBe(false); - strictEqual(program.diagnostics.length, 1); - strictEqual( - program.diagnostics[0].code, - "@typespec/http-client-csharp/invalid-dotnet-sdk-dependency", - ); - strictEqual( - program.diagnostics[0].message, - "The .NET SDK found is version 5.0.408. Please install the .NET SDK 8 or above and ensure there is no global.json in the file system requesting a lower version. Guidance for installing the .NET SDK can be found at https://dotnet.microsoft.com/.", - ); - }); -}); diff --git a/packages/http-client-csharp/emitter/test/Unit/validate-dotnet-sdk.test.ts b/packages/http-client-csharp/emitter/test/Unit/validate-dotnet-sdk.test.ts new file mode 100644 index 00000000000..a3f0eb397fd --- /dev/null +++ b/packages/http-client-csharp/emitter/test/Unit/validate-dotnet-sdk.test.ts @@ -0,0 +1,118 @@ +vi.resetModules(); + +import { Diagnostic, Program } from "@typespec/compiler"; +import { TestHost } from "@typespec/compiler/testing"; +import { strictEqual } from "assert"; +import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; +import { execAsync } from "../../src/lib/utils.js"; +import { + createCSharpSdkContext, + createEmitterContext, + createEmitterTestHost, + typeSpecCompile, +} from "./utils/test-util.js"; + +describe("Test _validateDotNetSdk", () => { + let runner: TestHost; + let program: Program; + const minVersion = 8; + let _validateDotNetSdk: (arg0: any, arg1: number) => Promise<[boolean, readonly Diagnostic[]]>; + + beforeEach(async () => { + vi.resetModules(); + runner = await createEmitterTestHost(); + program = await typeSpecCompile( + ` + op test( + @query + @encode(DurationKnownEncoding.ISO8601) + input: duration + ): NoContentResponse; + `, + runner, + ); + // Restore all mocks before each test + vi.restoreAllMocks(); + vi.mock("../../src/lib/utils.js", () => ({ + execCSharpGenerator: vi.fn(), + execAsync: vi.fn(), + })); + + // dynamically import the module to get the _validateDotNetSdk function + _validateDotNetSdk = (await import("../../src/emit-generate.js"))._validateDotNetSdk; + }); + + it("should return false and report diagnostic when dotnet SDK is not installed.", async () => { + const error: any = new Error("ENOENT: no such file or directory"); + error.code = "ENOENT"; + (execAsync as Mock).mockRejectedValueOnce(error); + const context = createEmitterContext(program); + const sdkContext = await createCSharpSdkContext(context); + const [result, diagnostics] = await _validateDotNetSdk(sdkContext, minVersion); + program.reportDiagnostics(diagnostics); + expect(result).toBe(false); + strictEqual(program.diagnostics.length, 1); + strictEqual( + program.diagnostics[0].code, + "@typespec/http-client-csharp/invalid-dotnet-sdk-dependency", + ); + strictEqual( + program.diagnostics[0].message, + "The dotnet command was not found in the PATH. Please install the .NET SDK version 8 or above. Guidance for installing the .NET SDK can be found at https://dotnet.microsoft.com/.", + ); + }); + + it("should return true for installed SDK version whose major equals min supported version", async () => { + (execAsync as Mock).mockResolvedValueOnce({ + exitCode: 0, + stdio: "", + stdout: "8.0.204", + stderr: "", + proc: { pid: 0, output: "", stdout: "", stderr: "", stdin: "" }, + }); + const context = createEmitterContext(program); + const sdkContext = await createCSharpSdkContext(context); + const [result, diagnostics] = await _validateDotNetSdk(sdkContext, minVersion); + expect(result).toBe(true); + strictEqual(diagnostics.length, 0); + }); + + it("should return true for installed SDK version whose major greaters than min supported version", async () => { + (execAsync as Mock).mockResolvedValueOnce({ + exitCode: 0, + stdio: "", + stdout: "9.0.102", + stderr: "", + proc: { pid: 0, output: "", stdout: "", stderr: "", stdin: "" }, + }); + const context = createEmitterContext(program); + const sdkContext = await createCSharpSdkContext(context); + const [result, diagnostics] = await _validateDotNetSdk(sdkContext, minVersion); + expect(result).toBe(true); + strictEqual(diagnostics.length, 0); + }); + + it("should return false and report diagnostic for invalid .NET SDK version", async () => { + (execAsync as Mock).mockResolvedValueOnce({ + exitCode: 0, + stdio: "", + stdout: "5.0.408", + stderr: "", + proc: { pid: 0, output: "", stdout: "", stderr: "", stdin: "" }, + }); + const context = createEmitterContext(program); + const sdkContext = await createCSharpSdkContext(context); + const [result, diagnostics] = await _validateDotNetSdk(sdkContext, minVersion); + program.reportDiagnostics(diagnostics); + expect(result).toBe(false); + strictEqual(program.diagnostics.length, 1); + strictEqual( + program.diagnostics[0].code, + "@typespec/http-client-csharp/invalid-dotnet-sdk-dependency", + ); + strictEqual( + program.diagnostics[0].message, + "The .NET SDK found is version 5.0.408. Please install the .NET SDK 8 or above and ensure there is no global.json in the file system requesting a lower version. Guidance for installing the .NET SDK can be found at https://dotnet.microsoft.com/.", + ); + }); +}); diff --git a/packages/http-client-csharp/eng/pipeline/publish.yml b/packages/http-client-csharp/eng/pipeline/publish.yml index fd1cf5ace9e..ac1f6075764 100644 --- a/packages/http-client-csharp/eng/pipeline/publish.yml +++ b/packages/http-client-csharp/eng/pipeline/publish.yml @@ -69,6 +69,10 @@ extends: LanguageShortName: "csharp" HasNugetPackages: true CadlRanchName: "@typespec/http-client-csharp" + UploadPlaygroundBundle: true + PlaygroundBundleBuildScript: "build:emitter" + PlaygroundServerDockerfile: "playground-server/Dockerfile" + PlaygroundServerAppName: "csharp-playground-server" AdditionalInitializeSteps: - task: UseDotNet@2 inputs: diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index 5b6d76d93b6..2287b15c23d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -10,6 +10,7 @@ using System.Threading; using Microsoft.TypeSpec.Generator.ClientModel.Primitives; using Microsoft.TypeSpec.Generator.ClientModel.Utilities; +using Microsoft.TypeSpec.Generator.EmitterRpc; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Input.Extensions; @@ -1112,16 +1113,21 @@ public ScmMethodProviderCollection GetMethodCollectionByOperation(InputOperation if (_backCompatProvider != backCompatProvider) { _backCompatProvider = backCompatProvider; - // reset cache so methods are rebuilt with the new backcompat provider - Reset(); + // Only reset the cached methods (and the underlying RestClient methods) + // so they are rebuilt with the new backcompat provider. Do NOT call full + // Reset() — that would discard properties/constructors/fields applied by + // visitors that may have already run (e.g., Azure DistributedTracingVisitor's + // ClientDiagnostics property). + ResetMethods(); + _restClient?.ResetMethods(); _methodCache = null; } } else if (_backCompatProvider != null) { - // backcompat provider was previously set but not requested now — reset to default _backCompatProvider = null; - Reset(); + ResetMethods(); + _restClient?.ResetMethods(); _methodCache = null; } _ = Methods; // Ensure methods are built @@ -1277,7 +1283,8 @@ protected sealed override IReadOnlyList BuildMethodsForBackCompa { methodsWithReorderedParams.Add(methodToUpdate); CodeModelGenerator.Instance.Emitter.Debug( - $"Preserved method {Name}.{methodToUpdate.Signature.Name} signature to match last contract."); + $"Reordered parameters of '{Name}.{methodToUpdate.Signature.Name}' to match last contract.", + BackCompatibilityChangeCategory.MethodParameterReordering); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelSerializationExtensionsDefinition.Xml.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelSerializationExtensionsDefinition.Xml.cs index a905391ece9..5dd058c335f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelSerializationExtensionsDefinition.Xml.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelSerializationExtensionsDefinition.Xml.cs @@ -218,12 +218,14 @@ private MethodProvider BuildXmlWriteObjectValueMethodProvider() new MethodBodyStatement[] { writer.WriteStartElement(nameHint), + writer.WriteAttributes(readerTyped, True), readerTyped.ReadStartElement(), writeNodeLoop, writer.WriteEndElement(), }, new MethodBodyStatement[] { + writer.WriteAttributes(readerTyped, True), readerTyped.ReadStartElement(), new WhileStatement(readerTyped.NodeType().NotEqual(new MemberExpression(typeof(XmlNodeType), nameof(XmlNodeType.EndElement)))) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 44fd9b7d549..11213a4f00f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -8,6 +8,7 @@ using System.Linq; using Microsoft.TypeSpec.Generator.ClientModel.Primitives; using Microsoft.TypeSpec.Generator.ClientModel.Snippets; +using Microsoft.TypeSpec.Generator.EmitterRpc; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Input.Extensions; @@ -15,6 +16,7 @@ using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Snippets; using Microsoft.TypeSpec.Generator.Statements; +using Microsoft.TypeSpec.Generator.Utilities; using static Microsoft.TypeSpec.Generator.Snippets.Snippet; namespace Microsoft.TypeSpec.Generator.ClientModel.Providers @@ -926,10 +928,24 @@ private static bool TryGetSpecialHeaderParam(InputParameter inputParameter, [Not : null; } - private static void UpdateParameterNameWithBackCompat(InputParameter inputParameter, string proposedName, TypeProvider backCompatProvider) + private static void UpdateParameterNameWithBackCompat(InputParameter inputParameter, string proposedName, TypeProvider backCompatProvider, InputServiceMethod? serviceMethod = null) { + // Look up the parameter's original (spec) name in the previous contract. + // When a service method is supplied, scope the search to methods whose name matches + // the current service method (allowing for sync/async pairing) so that a common + // parameter name (e.g. "id") on multiple methods can't cross-match. + var lastContractMethods = backCompatProvider.LastContractView?.Methods; + IEnumerable? scopedMethods = lastContractMethods; + if (lastContractMethods != null && serviceMethod != null) + { + var serviceMethodName = serviceMethod.Name; + scopedMethods = lastContractMethods.Where(m => + string.Equals(m.Signature.Name, serviceMethodName, StringComparison.OrdinalIgnoreCase) || + string.Equals(m.Signature.Name, serviceMethodName + "Async", StringComparison.OrdinalIgnoreCase)); + } + // Check if the original wire name exists in LastContractView for backward compatibility. - var existingParam = backCompatProvider.LastContractView?.Methods + var existingParam = scopedMethods ?.SelectMany(method => method.Signature.Parameters) .FirstOrDefault(p => string.Equals(p.Name, inputParameter.OriginalName, StringComparison.OrdinalIgnoreCase)) ?.Name; @@ -937,6 +953,12 @@ private static void UpdateParameterNameWithBackCompat(InputParameter inputParame if (existingParam != null) { // Preserve the exact name (including casing) from the previous contract for backward compatibility + if (!string.Equals(proposedName, existingParam, StringComparison.Ordinal)) + { + CodeModelGenerator.Instance.Emitter.Debug( + $"Preserved parameter name '{existingParam}' on '{backCompatProvider.Name}' from last contract (instead of '{proposedName}').", + BackCompatibilityChangeCategory.ParameterNamePreserved); + } proposedName = existingParam; } @@ -1064,12 +1086,10 @@ internal static List GetMethodParameters( // For paging operations, handle parameter name corrections with backward compatibility if (serviceMethod is InputPagingServiceMethod) { - var backCompatProvider = client.BackCompatProvider; - // Rename "top" parameter to "maxCount" (with backward compatibility). if (string.Equals(inputParam.OriginalName, TopParameterName, StringComparison.OrdinalIgnoreCase)) { - UpdateParameterNameWithBackCompat(inputParam, MaxCountParameterName, backCompatProvider); + UpdateParameterNameWithBackCompat(inputParam, MaxCountParameterName, client.BackCompatProvider, serviceMethod); } // Ensure page size parameter uses the correct casing (with backward compatibility) @@ -1080,10 +1100,17 @@ internal static List GetMethodParameters( : pageSizeParameterName; // For page size parameters, normalize badly-cased "maxpagesize" variants to proper camelCase, but always // respect backcompat. - UpdateParameterNameWithBackCompat(inputParam, updatedPageSizeParameterName, backCompatProvider); + UpdateParameterNameWithBackCompat(inputParam, updatedPageSizeParameterName, client.BackCompatProvider, serviceMethod); } } + // For every parameter, preserve a previously-published parameter name when the + // last contract has a matching parameter (matched by spec/original name). This + // generalizes back-compat name preservation beyond the paging-specific renames + // above so that any rename emitted by the generator falls back to the prior name + // when one was already published. + UpdateParameterNameWithBackCompat(inputParam, inputParam.Name, client.BackCompatProvider, serviceMethod); + ParameterProvider? parameter = ScmCodeModelGenerator.Instance.TypeFactory.CreateParameter(inputParam)?.ToPublicInputParameter(); if (parameter is null) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs index 73148e9ed9c..6e63a330e24 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs @@ -956,7 +956,7 @@ private ScmMethodProvider BuildProtocolMethod(MethodProvider createRequestMethod [ UsingDeclare("message", ScmCodeModelGenerator.Instance.TypeFactory.HttpMessageApi.HttpMessageType, This.Invoke(createRequestMethod.Signature, - [.. parameters]), out var message), + [.. parameters.Select(p => (ValueExpression)p)]), out var message), Return(ScmCodeModelGenerator.Instance.TypeFactory.ClientResponseApi.ToExpression().FromResponse(client .PipelineProperty.Invoke(processMessageName, [message, requestOptionsParameter], isAsync, true, extensionType: _clientPipelineExtensionsDefinition.Type))) ]; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmModelProvider.cs index 1faf1ae4044..31c2de56eb1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmModelProvider.cs @@ -9,12 +9,14 @@ using System.Linq; using System.Text.Json.Serialization; using Microsoft.TypeSpec.Generator.ClientModel.Snippets; +using Microsoft.TypeSpec.Generator.EmitterRpc; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Snippets; using Microsoft.TypeSpec.Generator.Statements; +using Microsoft.TypeSpec.Generator.Utilities; using static Microsoft.TypeSpec.Generator.Snippets.Snippet; namespace Microsoft.TypeSpec.Generator.ClientModel.Providers @@ -389,8 +391,17 @@ private bool BuildNeedsBackCompatAdditionalProperties() return false; } - return LastContractView.Properties.Any(p => + bool needsBackCompat = LastContractView.Properties.Any(p => p.Name == AdditionalPropertiesHelper.DefaultAdditionalPropertiesPropertyName); + + if (needsBackCompat) + { + CodeModelGenerator.Instance.Emitter.Debug( + $"Preserved 'AdditionalProperties' property shape on model '{Name}' to match last contract.", + BackCompatibilityChangeCategory.AdditionalPropertiesShapePreserved); + } + + return needsBackCompat; } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Snippets/XmlWriterSnippets.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Snippets/XmlWriterSnippets.cs index 32f0e379c9f..94478e36324 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Snippets/XmlWriterSnippets.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Snippets/XmlWriterSnippets.cs @@ -56,6 +56,9 @@ public static MethodBodyStatement WriteBase64StringValue(this ScopedApi writer, ValueExpression reader, ValueExpression defattr) => writer.Invoke(nameof(XmlWriter.WriteNode), [reader, defattr]).Terminate(); + public static MethodBodyStatement WriteAttributes(this ScopedApi writer, ValueExpression reader, ValueExpression defattr) + => writer.Invoke(nameof(XmlWriter.WriteAttributes), [reader, defattr]).Terminate(); + public static MethodBodyStatement WriteObjectValue(this ScopedApi writer, ScopedApi value, ValueExpression options, ValueExpression? nameHint = null) => ModelSerializationExtensionsSnippets.WriteObjectValue(writer, value, options, nameHint); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ModelReaderWriterValidation/TestProjects/Sample_TypeSpec/TestData/XmlAdvancedModel/XmlAdvancedModel.xml b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ModelReaderWriterValidation/TestProjects/Sample_TypeSpec/TestData/XmlAdvancedModel/XmlAdvancedModel.xml index b82754f6d5c..c541ee735e5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ModelReaderWriterValidation/TestProjects/Sample_TypeSpec/TestData/XmlAdvancedModel/XmlAdvancedModel.xml +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ModelReaderWriterValidation/TestProjects/Sample_TypeSpec/TestData/XmlAdvancedModel/XmlAdvancedModel.xml @@ -10,11 +10,11 @@ string2 10 20 - + Item 1 100 - + Item 2 200 @@ -23,7 +23,7 @@ blue - + Item 3 300 @@ -58,21 +58,21 @@ - + Item 1 100 - + Item 1 100 - + Item 1 100 @@ -80,7 +80,7 @@ - + Item 1 100 @@ -88,7 +88,7 @@ - + Item 1 100 diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/DeserializeXmlValueOverride_CustomTypeDeserialization.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/DeserializeXmlValueOverride_CustomTypeDeserialization.cs index 16d6c5bd4c0..edd2e53aec8 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/DeserializeXmlValueOverride_CustomTypeDeserialization.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/DeserializeXmlValueOverride_CustomTypeDeserialization.cs @@ -2,9 +2,7 @@ #nullable disable -using System; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.Xml.Linq; using Sample.Models; @@ -20,7 +18,6 @@ public partial class TestXmlModel } string name = default; - global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); foreach (var child in element.Elements()) { @@ -31,7 +28,7 @@ public partial class TestXmlModel continue; } } - return new global::Sample.Models.TestXmlModel(name, additionalBinaryDataProperties); + return new global::Sample.Models.TestXmlModel(name); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationForDiscriminatedSubtype.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationForDiscriminatedSubtype.cs index 1c8d7084b5b..3ce20e67bc3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationForDiscriminatedSubtype.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationForDiscriminatedSubtype.cs @@ -2,9 +2,7 @@ #nullable disable -using System; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.Xml.Linq; using Sample.Models; @@ -21,7 +19,6 @@ public partial class Cat string kind = "cat"; string name = default; - global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); bool meows = default; foreach (var child in element.Elements()) @@ -43,7 +40,7 @@ public partial class Cat continue; } } - return new global::Sample.Models.Cat(kind, name, additionalBinaryDataProperties, meows); + return new global::Sample.Models.Cat(kind, name, meows); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationHandlesAttributeProperties.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationHandlesAttributeProperties.cs index d78f351b8f9..7f3ef6443e6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationHandlesAttributeProperties.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationHandlesAttributeProperties.cs @@ -2,9 +2,7 @@ #nullable disable -using System; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.Xml.Linq; using Sample.Models; @@ -21,7 +19,6 @@ public partial class TestXmlModel string id = default; string name = default; - global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); foreach (var attr in element.Attributes()) { @@ -42,7 +39,7 @@ public partial class TestXmlModel continue; } } - return new global::Sample.Models.TestXmlModel(id, name, additionalBinaryDataProperties); + return new global::Sample.Models.TestXmlModel(id, name); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationHandlesAttributeWithNamespace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationHandlesAttributeWithNamespace.cs index 4f6df6e0015..64941a51d6a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationHandlesAttributeWithNamespace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationHandlesAttributeWithNamespace.cs @@ -2,9 +2,7 @@ #nullable disable -using System; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.Xml.Linq; using Sample.Models; @@ -24,7 +22,6 @@ public partial class TestXmlModel string id = default; string label = default; string name = default; - global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); foreach (var attr in element.Attributes()) { @@ -54,7 +51,7 @@ public partial class TestXmlModel continue; } } - return new global::Sample.Models.TestXmlModel(id, label, name, additionalBinaryDataProperties); + return new global::Sample.Models.TestXmlModel(id, label, name); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationHandlesElementWithNamespace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationHandlesElementWithNamespace.cs index 2b1beef65fa..eb5106d7d17 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationHandlesElementWithNamespace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationHandlesElementWithNamespace.cs @@ -2,9 +2,7 @@ #nullable disable -using System; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.Xml.Linq; using Sample.Models; @@ -23,7 +21,6 @@ public partial class TestXmlModel string id = default; string category = default; - global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); foreach (var child in element.Elements()) { @@ -41,7 +38,7 @@ public partial class TestXmlModel continue; } } - return new global::Sample.Models.TestXmlModel(id, category, additionalBinaryDataProperties); + return new global::Sample.Models.TestXmlModel(id, category); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodBodyContainsPropertyDeserialization.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodBodyContainsPropertyDeserialization.cs index fd9978a35ca..03e22dc1a92 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodBodyContainsPropertyDeserialization.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodBodyContainsPropertyDeserialization.cs @@ -4,10 +4,8 @@ using System; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.IO; using System.Xml.Linq; -using Sample; namespace Sample.Models { @@ -40,7 +38,6 @@ internal TestXmlModel() } string name = default; - global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); foreach (var child in element.Elements()) { @@ -51,7 +48,7 @@ internal TestXmlModel() continue; } } - return new global::Sample.Models.TestXmlModel(name, additionalBinaryDataProperties); + return new global::Sample.Models.TestXmlModel(name); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodHandlesNestedModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodHandlesNestedModel.cs index 38232184a0d..87fcc6f34f5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodHandlesNestedModel.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodHandlesNestedModel.cs @@ -4,10 +4,8 @@ using System; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.IO; using System.Xml.Linq; -using Sample; namespace Sample.Models { @@ -36,7 +34,6 @@ public partial class OuterModel : global::System.ClientModel.Primitives.IPersist } global::Sample.Models.InnerModel inner = default; - global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); foreach (var child in element.Elements()) { @@ -47,7 +44,7 @@ public partial class OuterModel : global::System.ClientModel.Primitives.IPersist continue; } } - return new global::Sample.Models.OuterModel(inner, additionalBinaryDataProperties); + return new global::Sample.Models.OuterModel(inner); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodHandlesOptionalUnwrappedListProperty.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodHandlesOptionalUnwrappedListProperty.cs index b39dde3d8a1..020e1a75d7f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodHandlesOptionalUnwrappedListProperty.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodHandlesOptionalUnwrappedListProperty.cs @@ -2,7 +2,6 @@ #nullable disable -using System; using System.ClientModel.Primitives; using System.Collections.Generic; using System.Xml.Linq; @@ -20,7 +19,6 @@ public partial class TestXmlModel } global::System.Collections.Generic.IList colors = default; - global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); foreach (var child in element.Elements()) { @@ -35,7 +33,7 @@ public partial class TestXmlModel continue; } } - return new global::Sample.Models.TestXmlModel((colors ?? new global::Sample.ChangeTrackingList()), additionalBinaryDataProperties); + return new global::Sample.Models.TestXmlModel((colors ?? new global::Sample.ChangeTrackingList())); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodHandlesUnwrappedListProperty.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodHandlesUnwrappedListProperty.cs index f0924206575..c8cd290f2ab 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodHandlesUnwrappedListProperty.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodHandlesUnwrappedListProperty.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.IO; using System.Xml.Linq; -using Sample; namespace Sample.Models { @@ -40,7 +39,6 @@ internal TestXmlModel() } global::System.Collections.Generic.IList colors = new global::System.Collections.Generic.List(); - global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); foreach (var child in element.Elements()) { @@ -51,7 +49,7 @@ internal TestXmlModel() continue; } } - return new global::Sample.Models.TestXmlModel(colors, additionalBinaryDataProperties); + return new global::Sample.Models.TestXmlModel(colors); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodHandlesWrappedListProperty.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodHandlesWrappedListProperty.cs index 4d90272f7c8..fea5d07ed14 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodHandlesWrappedListProperty.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlDeserializationTests/XmlDeserializationMethodHandlesWrappedListProperty.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.IO; using System.Xml.Linq; -using Sample; namespace Sample.Models { @@ -40,7 +39,6 @@ internal TestXmlModel() } global::System.Collections.Generic.IList counts = default; - global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); foreach (var child in element.Elements()) { @@ -56,7 +54,7 @@ internal TestXmlModel() continue; } } - return new global::Sample.Models.TestXmlModel(counts, additionalBinaryDataProperties); + return new global::Sample.Models.TestXmlModel(counts); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/CanChangePropertyName.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/CanChangePropertyName.cs index 57bd89ae9ec..d3d8c3cec4a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/CanChangePropertyName.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/CanChangePropertyName.cs @@ -4,7 +4,6 @@ using System; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.Xml; using System.Xml.Linq; using Sample.Models; @@ -37,7 +36,6 @@ internal virtual void XmlModelWriteCore(global::System.Xml.XmlWriter writer, glo } string prop2 = default; - global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); foreach (var child in element.Elements()) { @@ -48,7 +46,7 @@ internal virtual void XmlModelWriteCore(global::System.Xml.XmlWriter writer, glo continue; } } - return new global::Sample.Models.Model(prop2, additionalBinaryDataProperties); + return new global::Sample.Models.Model(prop2); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/CanCustomizeAttributeDeserializationMethod.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/CanCustomizeAttributeDeserializationMethod.cs index 0670d9d22c3..ae888a19096 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/CanCustomizeAttributeDeserializationMethod.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/CanCustomizeAttributeDeserializationMethod.cs @@ -2,9 +2,7 @@ #nullable disable -using System; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.Xml.Linq; using Sample.Models; @@ -21,7 +19,6 @@ public partial class MockInputModel string id = default; string name = default; - global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); foreach (var attr in element.Attributes()) { @@ -42,7 +39,7 @@ public partial class MockInputModel continue; } } - return new global::Sample.Models.MockInputModel(id, name, additionalBinaryDataProperties); + return new global::Sample.Models.MockInputModel(id, name); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/CanCustomizeDeserializationMethod.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/CanCustomizeDeserializationMethod.cs index 0cc66cbc057..2390e134531 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/CanCustomizeDeserializationMethod.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/CanCustomizeDeserializationMethod.cs @@ -2,9 +2,7 @@ #nullable disable -using System; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.Xml.Linq; using Sample.Models; @@ -21,7 +19,6 @@ public partial class MockInputModel string prop1 = default; string prop2 = default; - global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); foreach (var child in element.Elements()) { @@ -37,7 +34,7 @@ public partial class MockInputModel continue; } } - return new global::Sample.Models.MockInputModel(prop1, prop2, additionalBinaryDataProperties); + return new global::Sample.Models.MockInputModel(prop1, prop2); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/CanCustomizeDeserializationMethodWithOptions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/CanCustomizeDeserializationMethodWithOptions.cs index 365e808343b..f32a6cb87fa 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/CanCustomizeDeserializationMethodWithOptions.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/CanCustomizeDeserializationMethodWithOptions.cs @@ -2,9 +2,7 @@ #nullable disable -using System; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.Xml.Linq; using Sample.Models; @@ -21,7 +19,6 @@ public partial class MockInputModel string prop1 = default; string prop2 = default; - global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); foreach (var child in element.Elements()) { @@ -37,7 +34,7 @@ public partial class MockInputModel continue; } } - return new global::Sample.Models.MockInputModel(prop1, prop2, additionalBinaryDataProperties); + return new global::Sample.Models.MockInputModel(prop1, prop2); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs index 5ae9a8a010a..ad27b8cd3c8 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs @@ -362,6 +362,39 @@ public async Task ContentTypeAfterBodyInLastContractView() Assert.AreEqual("contentType", methodParameters[2].Name); // contentType after body } + [Test] + public async Task ParameterNamePreservedFromLastContractView() + { + // A non-paging, non-special parameter that the generator renames from "oldParam" to + // "newParam". The previously-published contract (TestData/.../TestClient.cs) declares + // the parameter as "oldParam", so backcompat should restore that name. + var queryParam = InputFactory.QueryParameter("oldParam", InputPrimitiveType.String, isRequired: true); + queryParam.Update(name: "newParam"); + Assert.AreEqual("newParam", queryParam.Name); + Assert.AreEqual("oldParam", queryParam.OriginalName); + + var operation = InputFactory.Operation("GetSomething", parameters: [queryParam]); + var serviceMethod = InputFactory.BasicServiceMethod("GetSomething", operation); + var client = InputFactory.Client("TestClient", methods: [serviceMethod]); + + var generator = await MockHelpers.LoadMockGeneratorAsync( + clients: () => [client], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var clientProvider = generator.Object.OutputLibrary.TypeProviders.OfType().FirstOrDefault(); + Assert.IsNotNull(clientProvider); + Assert.IsNotNull(clientProvider!.LastContractView); + + var protocolParams = RestClientProvider.GetMethodParameters(serviceMethod, ScmMethodKind.Protocol, clientProvider!); + + Assert.IsNotNull( + protocolParams.FirstOrDefault(p => string.Equals(p.Name, "oldParam", StringComparison.Ordinal)), + "Protocol parameter should be restored to the previously-published 'oldParam' name."); + Assert.IsNull( + protocolParams.FirstOrDefault(p => string.Equals(p.Name, "newParam", StringComparison.Ordinal)), + "When 'oldParam' is preserved, the renamed 'newParam' must not appear."); + } + [TestCase(true, true)] [TestCase(true, false)] [TestCase(false, true)] diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ParameterNamePreservedFromLastContractView/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ParameterNamePreservedFromLastContractView/TestClient.cs new file mode 100644 index 00000000000..b1d958b4120 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ParameterNamePreservedFromLastContractView/TestClient.cs @@ -0,0 +1,15 @@ +#nullable disable + +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + // Represents the previously published contract: parameter is named "oldParam". + public virtual Task GetSomethingAsync(string oldParam, RequestOptions options = null) { return null; } + public virtual ClientResult GetSomething(string oldParam, RequestOptions options = null) { return null; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/BackCompatibilityChangeCategory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/BackCompatibilityChangeCategory.cs new file mode 100644 index 00000000000..bc270e28a71 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/BackCompatibilityChangeCategory.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.TypeSpec.Generator.EmitterRpc +{ + /// + /// High-level category of a back-compatibility change applied due to a + /// library's last contract. Used by 's buffered logging + /// overloads to group entries when emitting the end-of-run summary. + /// + public enum BackCompatibilityChangeCategory + { + /// The order of parameters of a client method was changed to match the last contract. + MethodParameterReordering, + + /// A parameter name (e.g. paging parameter) was preserved from the last contract. + ParameterNamePreserved, + + /// The shape of a model's AdditionalProperties property was preserved from the last contract. + AdditionalPropertiesShapePreserved, + + /// A property type was preserved from the last contract. + PropertyTypePreserved, + + /// A constructor modifier (e.g. private protected -> public) was preserved from the last contract. + ConstructorModifierPreserved, + + /// The order of enum members was changed to match the last contract. + EnumMemberReordering, + + /// An API version enum member was added to preserve members from the last contract. + ApiVersionEnumMemberAdded, + + /// A model factory method was replaced to preserve the parameter order from the last contract. + ModelFactoryMethodReplaced, + + /// A back-compat overload of a model factory method was added based on the last contract. + ModelFactoryMethodAdded, + + /// A back-compat model factory method could not be created and was skipped. + ModelFactoryMethodSkipped, + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/Emitter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/Emitter.cs index 12fbc9312ce..b43c03385e5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/Emitter.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/Emitter.cs @@ -2,7 +2,10 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Text; using System.Text.Json; namespace Microsoft.TypeSpec.Generator.EmitterRpc @@ -12,11 +15,18 @@ public sealed class Emitter : IDisposable private const string BasicNotificationFormat = @"{{""method"":{0},""params"":{1}}}"; private const string Trace = "trace"; private const string Diagnostic = "diagnostic"; + private const string InfoLevel = "info"; + private const string DebugLevel = "debug"; + private const string VerboseLevel = "verbose"; private bool _disposed; private readonly StreamWriter _writer; + private readonly object _bufferLock = new(); + private readonly Dictionary>> _bufferedMessages = + new(StringComparer.Ordinal); + internal Emitter(Stream stream) { _writer = new StreamWriter(stream) { AutoFlush = true }; @@ -35,7 +45,7 @@ public void Info(string message) { SendNotification(Trace, new { - level = "info", + level = InfoLevel, message = message, }); } @@ -44,7 +54,7 @@ public void Debug(string message) { SendNotification(Trace, new { - level = "debug", + level = DebugLevel, message = message, }); } @@ -53,11 +63,126 @@ public void Verbose(string message) { SendNotification(Trace, new { - level = "verbose", + level = VerboseLevel, message = message, }); } + /// + /// Buffers an info-level message under the given . + /// Buffered messages are deduplicated per category and emitted as a single + /// grouped summary trace when is called or + /// when the emitter is disposed. + /// + public void Info(string message, BackCompatibilityChangeCategory category) => Buffer(InfoLevel, category, message); + + /// + /// Buffers a debug-level message under the given . + /// Buffered messages are deduplicated per category and emitted as a single + /// grouped summary trace when is called or + /// when the emitter is disposed. + /// + public void Debug(string message, BackCompatibilityChangeCategory category) => Buffer(DebugLevel, category, message); + + /// + /// Buffers a verbose-level message under the given . + /// Buffered messages are deduplicated per category and emitted as a single + /// grouped summary trace when is called or + /// when the emitter is disposed. + /// + public void Verbose(string message, BackCompatibilityChangeCategory category) => Buffer(VerboseLevel, category, message); + + private void Buffer(string level, BackCompatibilityChangeCategory category, string message) + { + if (string.IsNullOrEmpty(message)) + { + return; + } + + lock (_bufferLock) + { + if (!_bufferedMessages.TryGetValue(level, out var perCategory)) + { + perCategory = new Dictionary>(); + _bufferedMessages[level] = perCategory; + } + + if (!perCategory.TryGetValue(category, out var set)) + { + set = new SortedSet(StringComparer.Ordinal); + perCategory[category] = set; + } + + set.Add(message); + } + } + + /// + /// Writes any buffered, category-grouped log messages as a single trace + /// notification per level. Subsequent calls have no effect until new + /// messages are buffered. + /// + public void WriteBufferedMessages() + { + lock (_bufferLock) + { + if (_bufferedMessages.Count == 0) + { + return; + } + + foreach (var levelPair in _bufferedMessages) + { + int total = 0; + foreach (var set in levelPair.Value.Values) + { + total += set.Count; + } + + int categoryCount = levelPair.Value.Count; + var sb = new StringBuilder(); + sb.Append("Summary of grouped '").Append(levelPair.Key).Append("' messages: ") + .Append(total).Append(total == 1 ? " message across " : " messages across ") + .Append(categoryCount).AppendLine(categoryCount == 1 ? " category." : " categories."); + + var orderedCategories = levelPair.Value + .Select(kvp => (Display: GetCategoryDisplayName(kvp.Key), Messages: kvp.Value)) + .OrderBy(x => x.Display, StringComparer.Ordinal); + foreach (var (display, messages) in orderedCategories) + { + sb.Append(" ").Append(display).Append(" (").Append(messages.Count).AppendLine("):"); + foreach (var msg in messages) + { + sb.Append(" - ").AppendLine(msg); + } + } + + SendNotification(Trace, new + { + level = levelPair.Key, + message = sb.ToString().TrimEnd(), + }); + } + + _bufferedMessages.Clear(); + } + } + + private static string GetCategoryDisplayName(BackCompatibilityChangeCategory category) => category switch + { + BackCompatibilityChangeCategory.MethodParameterReordering => "Method Parameter Reordering", + BackCompatibilityChangeCategory.ParameterNamePreserved => "Parameter Name Preserved", + BackCompatibilityChangeCategory.AdditionalPropertiesShapePreserved => "AdditionalProperties Shape Preserved", + BackCompatibilityChangeCategory.PropertyTypePreserved => "Property Type Preserved", + BackCompatibilityChangeCategory.ConstructorModifierPreserved => "Constructor Modifier Preserved", + BackCompatibilityChangeCategory.EnumMemberReordering => "Enum Member Reordering", + BackCompatibilityChangeCategory.ApiVersionEnumMemberAdded => "Api Version Enum Member Added From Last Contract", + BackCompatibilityChangeCategory.ModelFactoryMethodReplaced => "Model Factory Method Replaced For Back-Compat", + BackCompatibilityChangeCategory.ModelFactoryMethodAdded => "Model Factory Method Added For Back-Compat", + BackCompatibilityChangeCategory.ModelFactoryMethodSkipped => "Model Factory Method Back-Compat Skipped", + _ => category.ToString(), + }; + public void ReportDiagnostic(string code, string message, string? targetCrossLanguageDefinitionId = null, EmitterDiagnosticSeverity severity = EmitterDiagnosticSeverity.Warning) { if (targetCrossLanguageDefinitionId != null) @@ -97,6 +222,7 @@ private void Dispose(bool disposing) { if (disposing) { + WriteBufferedMessages(); _writer.Dispose(); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Expressions/ValueExpression.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Expressions/ValueExpression.cs index 0e37d14cd15..0dde202a62f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Expressions/ValueExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Expressions/ValueExpression.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Snippets; @@ -80,7 +81,7 @@ public InvokeMethodExpression Invoke(string methodName, IReadOnlyList new InvokeMethodExpression(this, methodName, arguments); public InvokeMethodExpression Invoke(MethodSignature methodSignature) - => new InvokeMethodExpression(this, methodSignature, [.. methodSignature.Parameters]) + => new InvokeMethodExpression(this, methodSignature, [.. methodSignature.Parameters.Select(p => (ValueExpression)p)]) { CallAsAsync = methodSignature.Modifiers.HasFlag(MethodSignatureModifiers.Async) }; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ApiVersionEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ApiVersionEnumProvider.cs index 8bb38365343..870a7aa77ef 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ApiVersionEnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ApiVersionEnumProvider.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using Microsoft.TypeSpec.Generator.EmitterRpc; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Input.Extensions; @@ -156,6 +157,9 @@ private List BuildApiVersionEnumValuesForBackwardCompatibility(L string enumValue = field.Name.ToApiVersionValue(versionPrefix, versionSeparator); allMembers.Add(new EnumTypeMember(field.Name, field, enumValue)); addedPreviousApiVersion = true; + CodeModelGenerator.Instance.Emitter.Debug( + $"Added previous API version '{field.Name}' to enum '{Name}' to preserve members from last contract.", + BackCompatibilityChangeCategory.ApiVersionEnumMemberAdded); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/FixedEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/FixedEnumProvider.cs index aa3d5adcede..713100c1cf1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/FixedEnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/FixedEnumProvider.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.TypeSpec.Generator.EmitterRpc; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Input.Extensions; @@ -125,9 +126,36 @@ protected override IReadOnlyList BuildEnumValues() } } + // Report a summary-level change only if the relative order of shared members + // was actually altered to match the last contract. + if (!EnumMemberOrderMatches(currentValues, allMembers)) + { + CodeModelGenerator.Instance.Emitter.Debug( + $"Reordered members of enum '{Name}' to match last contract.", + BackCompatibilityChangeCategory.EnumMemberReordering); + } + return allMembers; } + private static bool EnumMemberOrderMatches( + IReadOnlyList left, + IReadOnlyList right) + { + if (left.Count != right.Count) + { + return false; + } + for (int i = 0; i < left.Count; i++) + { + if (!string.Equals(left[i].Name, right[i].Name, StringComparison.Ordinal)) + { + return false; + } + } + return true; + } + protected internal override FieldProvider[] BuildFields() => EnumValues.Select(v => v.Field).ToArray(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs index c4ed418c3b6..79b8e58ac42 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs @@ -6,11 +6,13 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using Microsoft.TypeSpec.Generator.EmitterRpc; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Snippets; using Microsoft.TypeSpec.Generator.Statements; +using Microsoft.TypeSpec.Generator.Utilities; using static Microsoft.TypeSpec.Generator.Snippets.Snippet; namespace Microsoft.TypeSpec.Generator.Providers @@ -97,7 +99,14 @@ protected internal sealed override IReadOnlyList BuildMethodsFor } List factoryMethods = [.. originalMethods]; - HashSet currentMethodSignatures = new List([.. originalMethods, .. CustomCodeView?.Methods ?? []]) + + // Preserve the original parameter names on current factory methods when the only + // change between the previous and current contract is a parameter rename. This + // avoids source-breaking changes for callers using named arguments (e.g. when a + // property is renamed via @@clientName, spec rename, or naming-rule change). + PreservePreviousParameterNames(factoryMethods); + + HashSet currentMethodSignatures = new List([.. factoryMethods, .. CustomCodeView?.Methods ?? []]) .Select(m => m.Signature) .ToHashSet(MethodSignature.MethodSignatureComparer); @@ -146,6 +155,10 @@ protected internal sealed override IReadOnlyList BuildMethodsFor factoryMethods.Remove(factoryMethodToRemove); } + CodeModelGenerator.Instance.Emitter.Debug( + $"Replaced model factory method '{Name}.{currentOverload.Name}' with previous parameter order from last contract.", + BackCompatibilityChangeCategory.ModelFactoryMethodReplaced); + foundCompatibleOverload = true; break; } @@ -153,6 +166,9 @@ protected internal sealed override IReadOnlyList BuildMethodsFor if (TryBuildCompatibleMethodForPreviousContract(previousMethod, currentOverload, true, out replacedMethod)) { factoryMethods.Add(replacedMethod); + CodeModelGenerator.Instance.Emitter.Debug( + $"Added back-compat overload for model factory method '{Name}.{previousMethod.Signature.Name}' delegating to '{currentOverload.Name}'.", + BackCompatibilityChangeCategory.ModelFactoryMethodAdded); foundCompatibleOverload = true; break; } @@ -167,16 +183,79 @@ protected internal sealed override IReadOnlyList BuildMethodsFor if (TryBuildCompatibleMethodForPreviousContract(previousMethod, null, true, out var builtMethod)) { factoryMethods.Add(builtMethod); + CodeModelGenerator.Instance.Emitter.Debug( + $"Added back-compat model factory method '{Name}.{previousMethod.Signature.Name}' from last contract.", + BackCompatibilityChangeCategory.ModelFactoryMethodAdded); } else { - CodeModelGenerator.Instance.Emitter.Info($"Unable to create a backward compatible model factory method for {previousMethod.Signature.FullMethodName}."); + CodeModelGenerator.Instance.Emitter.Info( + $"Unable to create a backward compatible model factory method for '{previousMethod.Signature.FullMethodName}'.", + BackCompatibilityChangeCategory.ModelFactoryMethodSkipped); } } return [.. factoryMethods]; } + // Preserve original parameter names from the previous contract when a current factory + // method matches a previous one by name + parameter types/order but differs only in + // parameter names. The rename is applied in-place via ParameterProvider.Update which + // also updates the cached variable/argument expressions used by the method body and + // XML docs, so the body and docs serialize with the preserved names automatically. + private void PreservePreviousParameterNames(List currentFactoryMethods) + { + if (LastContractView?.Methods == null) + { + return; + } + + foreach (var previousMethod in LastContractView.Methods) + { + MethodProvider? matchingCurrent = null; + foreach (var current in currentFactoryMethods) + { + // MethodSignatureComparer matches on method name + parameter count + parameter + // types (positional); it does not consider parameter names. So a previous + // method whose only difference from a current method is parameter names will + // still match here. + if (MethodSignature.MethodSignatureComparer.Equals(current.Signature, previousMethod.Signature)) + { + matchingCurrent = current; + break; + } + } + + if (matchingCurrent is null) + { + continue; + } + + var previousParameters = previousMethod.Signature.Parameters; + var currentParameters = matchingCurrent.Signature.Parameters; + if (previousParameters.Count != currentParameters.Count) + { + continue; + } + + for (int i = 0; i < previousParameters.Count; i++) + { + var previousName = previousParameters[i].Name; + var currentParam = currentParameters[i]; + if (string.IsNullOrEmpty(previousName) || currentParam.Name == previousName) + { + continue; + } + + CodeModelGenerator.Instance.Emitter.Debug( + $"Preserved parameter name '{previousName}' on '{Name}.{matchingCurrent.Signature.Name}' from last contract (instead of '{currentParam.Name}').", + BackCompatibilityChangeCategory.ParameterNamePreserved); + + currentParam.Update(name: previousName); + } + } + } + private bool TryBuildCompatibleMethodForPreviousContract( MethodProvider previousMethod, MethodSignature? currentMethodSignature, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index 3f177c64425..fb0e13aa7be 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using Microsoft.TypeSpec.Generator.EmitterRpc; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Input.Extensions; @@ -757,6 +758,9 @@ protected internal override IReadOnlyList BuildConstructors currentConstructor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Protected)) { currentConstructor.Signature.Update(modifiers: MethodSignatureModifiers.Public); + CodeModelGenerator.Instance.Emitter.Debug( + $"Promoted constructor '{Name}({string.Join(", ", currentConstructor.Signature.Parameters.Select(p => p.Type.ToString()))})' from 'private protected' to 'public' to match last contract.", + BackCompatibilityChangeCategory.ConstructorModifierPreserved); } } } @@ -1207,6 +1211,11 @@ private ValueExpression GetConversion(PropertyProvider? property = default, Fiel /// The constructed if the model should generate the field. private FieldProvider? BuildRawDataField() { + if (_inputModel.Usage.HasFlag(InputModelTypeUsage.Xml) && !_inputModel.Usage.HasFlag(InputModelTypeUsage.Json)) + { + return null; + } + // check if there is a raw data field on any of the base models, if so, we do not have to have one here. var baseModelProvider = BaseModelProvider; while (baseModelProvider != null) @@ -1283,7 +1292,9 @@ protected internal override IReadOnlyList BuildPropertiesForBa var parameter = outputProperty.AsParameter; parameter.Update(type: newType); parameter.ToPublicInputParameter().Update(type: newType.InputType); - CodeModelGenerator.Instance.Emitter.Info($"Changed property {Name}.{outputProperty.Name} type to {lastContractPropertyType} to match last contract."); + CodeModelGenerator.Instance.Emitter.Info( + $"Changed property '{Name}.{outputProperty.Name}' type to '{lastContractPropertyType}' to match last contract.", + BackCompatibilityChangeCategory.PropertyTypePreserved); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 052a4c84157..911fb12103c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -402,6 +402,17 @@ protected virtual XmlDocProvider BuildXmlDocs() protected abstract string BuildRelativeFilePath(); protected abstract string BuildName(); + /// + /// Resets only the cached methods so they are rebuilt on next access. + /// Use this instead of when you need to force a method + /// rebuild without discarding visitor-applied state on properties, fields, + /// constructors, or canonical/last-contract views. + /// + public void ResetMethods() + { + _methods = null; + } + /// /// Resets the type provider to its initial state, clearing all cached properties and fields. /// This allows for the type provider to rebuild its state on subsequent calls to its properties. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/EmitterRpc/EmitterTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/EmitterRpc/EmitterTests.cs index cf14c18f7cb..8e01aa4d691 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/EmitterRpc/EmitterTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/EmitterRpc/EmitterTests.cs @@ -68,6 +68,82 @@ public void TestReportDiagnosticWithoutTarget() Assert.AreEqual(@"{""method"":""diagnostic"",""params"":{""code"":""test-code"",""message"":""Test message"",""severity"":""warning""}}", GetResult()); } + [TestCase] + public void WriteBufferedMessages_NoBufferedMessages_WritesNothing() + { + _emitter?.WriteBufferedMessages(); + Assert.AreEqual(string.Empty, GetResult()); + } + + [TestCase] + public void BufferedDebug_GroupsMessagesByCategory() + { + _emitter?.Debug("Reordered parameters of ClientA.DoThing.", BackCompatibilityChangeCategory.MethodParameterReordering); + _emitter?.Debug("Reordered parameters of ClientB.DoOther.", BackCompatibilityChangeCategory.MethodParameterReordering); + _emitter?.Debug("Preserved parameter name top on ClientA.", BackCompatibilityChangeCategory.ParameterNamePreserved); + + // Nothing should be written until WriteBufferedMessages is called. + Assert.AreEqual(0, _stream!.Length); + + _emitter?.WriteBufferedMessages(); + var result = GetResult(); + StringAssert.Contains(@"""level"":""debug""", result); + StringAssert.Contains("3 messages across 2 categories", result); + StringAssert.Contains("Method Parameter Reordering (2):", result); + StringAssert.Contains("Parameter Name Preserved (1):", result); + StringAssert.Contains("Reordered parameters of ClientA.DoThing.", result); + StringAssert.Contains("Reordered parameters of ClientB.DoOther.", result); + StringAssert.Contains("Preserved parameter name top on ClientA.", result); + } + + [TestCase] + public void BufferedDebug_DeduplicatesIdenticalEntries() + { + _emitter?.Debug("same", BackCompatibilityChangeCategory.MethodParameterReordering); + _emitter?.Debug("same", BackCompatibilityChangeCategory.MethodParameterReordering); + _emitter?.Debug("same", BackCompatibilityChangeCategory.MethodParameterReordering); + + _emitter?.WriteBufferedMessages(); + var result = GetResult(); + StringAssert.Contains("1 message across 1 category", result); + StringAssert.Contains("Method Parameter Reordering (1):", result); + } + + [TestCase] + public void BufferedDebug_IgnoresNullOrEmptyMessage() + { + _emitter?.Debug("", BackCompatibilityChangeCategory.MethodParameterReordering); + _emitter?.Debug(null!, BackCompatibilityChangeCategory.MethodParameterReordering); + _emitter?.WriteBufferedMessages(); + + Assert.AreEqual(string.Empty, GetResult()); + } + + [TestCase] + public void BufferedMessages_DifferentLevelsEmittedSeparately() + { + _emitter?.Info("info-msg", BackCompatibilityChangeCategory.MethodParameterReordering); + _emitter?.Debug("debug-msg", BackCompatibilityChangeCategory.MethodParameterReordering); + + _emitter?.WriteBufferedMessages(); + var result = GetResult(); + StringAssert.Contains(@"""level"":""info""", result); + StringAssert.Contains(@"""level"":""debug""", result); + StringAssert.Contains("info-msg", result); + StringAssert.Contains("debug-msg", result); + } + + [TestCase] + public void WriteBufferedMessages_ClearsBuffer() + { + _emitter?.Debug("once", BackCompatibilityChangeCategory.MethodParameterReordering); + _emitter?.WriteBufferedMessages(); + + _stream?.SetLength(0); + _emitter?.WriteBufferedMessages(); + Assert.AreEqual(string.Empty, GetResult()); + } + private string GetResult() { _stream?.Seek(0, SeekOrigin.Begin); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs index c5948c7a003..ac5085b8f40 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs @@ -439,6 +439,169 @@ public async Task BackCompatibility_ExactMatchWithCompatibleOverload() result); } + [Test] + public async Task BackCompatibility_OnlyParamNameChanged() + { + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: ModelList, + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync())).Object; + + var modelFactory = _instance!.OutputLibrary.ModelFactory.Value; + Assert.AreEqual("SampleNamespaceModelFactory", modelFactory.Name); + + modelFactory.ProcessTypeForBackCompatibility(); + + var methods = modelFactory.Methods; + // No additional overload should be added — the rename is absorbed into the current method. + Assert.AreEqual(ModelList.Length - ModelList.Where(m => m.Access == "internal").Count(), methods.Count); + + var publicModel1Methods = methods.Where(m => m.Signature.Name == "PublicModel1").ToList(); + Assert.AreEqual(1, publicModel1Methods.Count); + + var publicModel1Method = publicModel1Methods[0]; + // Previous parameter names should be preserved on the current method. + var parameters = publicModel1Method.Signature.Parameters; + Assert.AreEqual(4, parameters.Count); + Assert.AreEqual("oldStringProp", parameters[0].Name); + Assert.AreEqual("oldModelProp", parameters[1].Name); + Assert.AreEqual("listProp", parameters[2].Name); + Assert.AreEqual("dictProp", parameters[3].Name); + + // No EditorBrowsable hidden overload — there's a single visible method. + Assert.AreEqual(0, publicModel1Method.Signature.Attributes.Count); + + // The body should reference the renamed parameters when constructing the model. + var body = publicModel1Method.BodyStatements; + Assert.IsNotNull(body); + var result = body!.ToDisplayString(); + Assert.AreEqual( + "listProp ??= new global::Sample.Namespace.ChangeTrackingList();\n" + + "dictProp ??= new global::Sample.Namespace.ChangeTrackingDictionary();\n\n" + + "return new global::Sample.Models.PublicModel1(oldStringProp, oldModelProp, listProp.ToList(), dictProp, additionalBinaryDataProperties: null);\n", + result); + + // The XML doc entries should also use the preserved names. + var docParams = publicModel1Method.XmlDocs!.Parameters; + Assert.AreEqual(parameters.Count, docParams.Count); + Assert.AreEqual("oldStringProp", docParams[0].Parameter.Name); + Assert.AreEqual("oldModelProp", docParams[1].Parameter.Name); + } + + // Validates that when ALL parameters in a factory method are renamed in the previous + // contract, every preserved name is propagated to the current method. This complements + // BackCompatibility_OnlyParamNameChanged which exercises a partial rename. + [Test] + public async Task BackCompatibility_MultipleParamNamesChanged() + { + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: ModelList, + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync())).Object; + + var modelFactory = _instance!.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var methods = modelFactory.Methods; + // No additional overload should be added — the renames are absorbed into the current method. + Assert.AreEqual(ModelList.Length - ModelList.Where(m => m.Access == "internal").Count(), methods.Count); + + var publicModel1Methods = methods.Where(m => m.Signature.Name == "PublicModel1").ToList(); + Assert.AreEqual(1, publicModel1Methods.Count); + var publicModel1Method = publicModel1Methods[0]; + + var parameters = publicModel1Method.Signature.Parameters; + Assert.AreEqual(4, parameters.Count); + Assert.AreEqual("previousStringProp", parameters[0].Name); + Assert.AreEqual("previousModelProp", parameters[1].Name); + Assert.AreEqual("previousListProp", parameters[2].Name); + Assert.AreEqual("previousDictProp", parameters[3].Name); + + // No EditorBrowsable hidden overload — there's a single visible method. + Assert.AreEqual(0, publicModel1Method.Signature.Attributes.Count); + + // The body should reference the renamed parameters when constructing the model. + var body = publicModel1Method.BodyStatements; + Assert.IsNotNull(body); + var result = body!.ToDisplayString(); + Assert.AreEqual( + "previousListProp ??= new global::Sample.Namespace.ChangeTrackingList();\n" + + "previousDictProp ??= new global::Sample.Namespace.ChangeTrackingDictionary();\n\n" + + "return new global::Sample.Models.PublicModel1(previousStringProp, previousModelProp, previousListProp.ToList(), previousDictProp, additionalBinaryDataProperties: null);\n", + result); + + // The XML doc entries should also use the preserved names. + var docParams = publicModel1Method.XmlDocs!.Parameters; + Assert.AreEqual(parameters.Count, docParams.Count); + Assert.AreEqual("previousStringProp", docParams[0].Parameter.Name); + Assert.AreEqual("previousModelProp", docParams[1].Parameter.Name); + Assert.AreEqual("previousListProp", docParams[2].Parameter.Name); + Assert.AreEqual("previousDictProp", docParams[3].Parameter.Name); + } + + // Validates that when a new property is added AND the previous contract used different + // names for some of the surviving parameters, the rename-only fast path does NOT apply + // (parameter counts differ). Instead the standard "new property added" backcompat overload + // is generated using the previously-published parameter names. + [Test] + public async Task BackCompatibility_NewPropertyAddedWithRenamedParam() + { + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: ModelList, + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync())).Object; + + var modelFactory = _instance!.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var methods = modelFactory.Methods; + // There should be an additional method for backward compatibility. + Assert.AreEqual(ModelList.Length - ModelList.Where(m => m.Access == "internal").Count() + 1, methods.Count); + + var currentOverloadMethod = methods + .FirstOrDefault(m => m.Signature.Name == "PublicModel1" && m.Signature.Parameters.Any(p => p.Name == "dictProp")); + var backwardCompatibilityMethod = methods + .FirstOrDefault(m => m.Signature.Name == "PublicModel1" && m.Signature.Parameters.All(p => p.Name != "dictProp")); + Assert.IsNotNull(currentOverloadMethod); + Assert.IsNotNull(backwardCompatibilityMethod); + + // The current method keeps the new property-derived names (no rename absorbed) since + // the parameter count differs between the current and previous methods. + var currentParameters = currentOverloadMethod!.Signature.Parameters; + Assert.AreEqual(4, currentParameters.Count); + Assert.AreEqual("stringProp", currentParameters[0].Name); + Assert.AreEqual("modelProp", currentParameters[1].Name); + Assert.AreEqual("listProp", currentParameters[2].Name); + Assert.AreEqual("dictProp", currentParameters[3].Name); + + // The backcompat overload preserves the previously-published parameter names. + var attributes = backwardCompatibilityMethod!.Signature.Attributes; + Assert.AreEqual(1, attributes.Count); + Assert.AreEqual( + "[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]\n", + attributes[0].ToDisplayString()); + + var parameters = backwardCompatibilityMethod.Signature.Parameters; + Assert.AreEqual(3, parameters.Count); + Assert.AreEqual("oldStringProp", parameters[0].Name); + Assert.AreEqual("oldModelProp", parameters[1].Name); + Assert.AreEqual("listProp", parameters[2].Name); + foreach (var param in parameters) + { + Assert.IsNull(param.DefaultValue); + } + + // The backcompat overload's body instantiates the model directly because the previous + // parameter names (oldStringProp, oldModelProp) do not match any current property name. + // For unmatched parameters the generator falls back to passing `default` to the + // constructor; matched names (listProp) are threaded through. + var body = backwardCompatibilityMethod.BodyStatements; + Assert.IsNotNull(body); + var bodyString = body!.ToDisplayString(); + StringAssert.Contains("listProp ??= new global::Sample.Namespace.ChangeTrackingList();", bodyString); + StringAssert.Contains("return new global::Sample.Models.PublicModel1(default, default, listProp.ToList(), default, additionalBinaryDataProperties: null);", bodyString); + } + [Test] public void ModelWithNestedDiscriminators() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_MultipleParamNamesChanged/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_MultipleParamNamesChanged/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..48683d57d93 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_MultipleParamNamesChanged/SampleNamespaceModelFactory.cs @@ -0,0 +1,26 @@ +using SampleTypeSpec; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + // Previous contract renamed ALL parameters of the factory method. The current + // method should preserve every previous parameter name. + public static PublicModel1 PublicModel1( + string previousStringProp = default, + Thing previousModelProp = default, + IEnumerable previousListProp = default, + IDictionary previousDictProp = default) + { } + } +} + +namespace Sample.Models +{ + public partial class Thing + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_NewPropertyAddedWithRenamedParam/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_NewPropertyAddedWithRenamedParam/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..f7e5036ba93 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_NewPropertyAddedWithRenamedParam/SampleNamespaceModelFactory.cs @@ -0,0 +1,30 @@ +using SampleTypeSpec; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + // Previous contract had only three of the four current properties AND the first two were + // exposed under different parameter names. Because the parameter count differs from the + // current method, the rename-only fast path does not apply; the standard "new property + // added" overload is generated using the previously-published names. + public static PublicModel1 PublicModel1( + string oldStringProp = default, + Thing oldModelProp = default, + IEnumerable listProp = default) + { } + } +} + +namespace Sample.Models +{ + public partial class PublicModel1 + { } + + public partial class Thing + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_OnlyParamNameChanged/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_OnlyParamNameChanged/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..a9d8aed3056 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_OnlyParamNameChanged/SampleNamespaceModelFactory.cs @@ -0,0 +1,27 @@ +using SampleTypeSpec; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + // Previous contract used different parameter names for two of the parameters. + // The current method should preserve the previous names rather than renaming them + // to the new property-derived names. + public static PublicModel1 PublicModel1( + string oldStringProp = default, + Thing oldModelProp = default, + IEnumerable listProp = default, + IDictionary dictProp = default) + { } + } +} + +namespace Sample.Models +{ + public partial class Thing + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs index 2f1fa49b1b7..a33a1a88de3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs @@ -1973,5 +1973,32 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.AreEqual(typeof(string), propertyType.Arguments[0].FrameworkType, "Key type should be string"); Assert.AreEqual(typeof(object), propertyType.Arguments[1].FrameworkType, "Value type should be object for backward compatibility"); } + + [TestCase(InputModelTypeUsage.Output | InputModelTypeUsage.Xml, false, TestName = "XmlOnly_OutputOnly_NoField")] + [TestCase(InputModelTypeUsage.Input | InputModelTypeUsage.Xml, false, TestName = "XmlOnly_Input_NoField")] + [TestCase(InputModelTypeUsage.Input | InputModelTypeUsage.Output | InputModelTypeUsage.Xml, false, TestName = "XmlOnly_InputAndOutput_NoField")] + [TestCase(InputModelTypeUsage.Output | InputModelTypeUsage.Json | InputModelTypeUsage.Xml, true, TestName = "JsonAndXml_Output_HasField")] + [TestCase(InputModelTypeUsage.Input | InputModelTypeUsage.Output | InputModelTypeUsage.Json | InputModelTypeUsage.Xml, true, TestName = "JsonAndXml_InputAndOutput_HasField")] + [TestCase(InputModelTypeUsage.Output | InputModelTypeUsage.Json, true, TestName = "JsonOnly_Output_HasField")] + public void TestBuildRawDataField_BasedOnUsage(InputModelTypeUsage usage, bool shouldHaveField) + { + var inputModel = InputFactory.Model( + "TestModel", + usage: usage, + properties: [InputFactory.Property("Name", InputPrimitiveType.String)]); + MockHelpers.LoadMockGenerator(inputModelTypes: [inputModel]); + + var modelProvider = new ModelProvider(inputModel); + + var rawDataField = modelProvider.Fields.FirstOrDefault(f => f.Name == "_additionalBinaryDataProperties"); + if (shouldHaveField) + { + Assert.IsNotNull(rawDataField, "Expected _additionalBinaryDataProperties field to be generated"); + } + else + { + Assert.IsNull(rawDataField, "Expected _additionalBinaryDataProperties field to NOT be generated for XML-only models"); + } + } } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/ModelSerializationExtensions.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/ModelSerializationExtensions.cs index 2fa84356c07..d8d35cf0e14 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/ModelSerializationExtensions.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/ModelSerializationExtensions.cs @@ -402,6 +402,7 @@ public static void WriteObjectValue(this XmlWriter writer, T value, ModelRead if (nameHint != null) { writer.WriteStartElement(nameHint); + writer.WriteAttributes(reader, true); reader.ReadStartElement(); while (reader.NodeType != XmlNodeType.EndElement) { @@ -411,6 +412,7 @@ public static void WriteObjectValue(this XmlWriter writer, T value, ModelRead } else { + writer.WriteAttributes(reader, true); reader.ReadStartElement(); while (reader.NodeType != XmlNodeType.EndElement) { diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.Serialization.cs index 82cb1a9d4f8..f1d688b9098 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.Serialization.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.Serialization.cs @@ -409,7 +409,6 @@ internal static XmlAdvancedModel DeserializeXmlAdvancedModel(XElement element, M IDictionary> dictionaryOfDictionaryFoo = default; IDictionary> dictionaryListFoo = default; IList> listOfDictionaryFoo = default; - IDictionary additionalBinaryDataProperties = new ChangeTrackingDictionary(); foreach (var attr in element.Attributes()) { @@ -736,8 +735,7 @@ internal static XmlAdvancedModel DeserializeXmlAdvancedModel(XElement element, M dictionaryFoo, dictionaryOfDictionaryFoo, dictionaryListFoo, - listOfDictionaryFoo, - additionalBinaryDataProperties); + listOfDictionaryFoo); } } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.cs index afffaa39cda..32a82daf882 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.cs @@ -15,9 +15,6 @@ namespace SampleTypeSpec /// An advanced XML model for testing various property types and XML features. public partial class XmlAdvancedModel { - /// Keeps track of any properties unknown to the library. - private protected readonly IDictionary _additionalBinaryDataProperties; - /// Initializes a new instance of . /// A simple string property. /// An integer property. @@ -158,8 +155,7 @@ public XmlAdvancedModel(string name, int age, bool enabled, float score, string /// /// /// - /// Keeps track of any properties unknown to the library. - internal XmlAdvancedModel(string name, int age, bool enabled, float score, string optionalString, int? optionalInt, string nullableString, string id, int version, bool isActive, string originalName, string xmlIdentifier, string content, IList unwrappedStrings, IList unwrappedCounts, IList unwrappedItems, IList wrappedColors, IList items, XmlNestedModel nestedModel, XmlNestedModel optionalNestedModel, IDictionary metadata, DateTimeOffset createdAt, TimeSpan duration, BinaryData data, IDictionary optionalRecordUnknown, StringFixedEnum fixedEnum, StringExtensibleEnum extensibleEnum, IntFixedEnum? optionalFixedEnum, IntExtensibleEnum? optionalExtensibleEnum, string label, int daysUsed, IList fooItems, XmlNestedModel anotherModel, IList modelsWithNamespaces, IList unwrappedModelsWithNamespaces, IList> listOfListFoo, IDictionary dictionaryFoo, IDictionary> dictionaryOfDictionaryFoo, IDictionary> dictionaryListFoo, IList> listOfDictionaryFoo, IDictionary additionalBinaryDataProperties) + internal XmlAdvancedModel(string name, int age, bool enabled, float score, string optionalString, int? optionalInt, string nullableString, string id, int version, bool isActive, string originalName, string xmlIdentifier, string content, IList unwrappedStrings, IList unwrappedCounts, IList unwrappedItems, IList wrappedColors, IList items, XmlNestedModel nestedModel, XmlNestedModel optionalNestedModel, IDictionary metadata, DateTimeOffset createdAt, TimeSpan duration, BinaryData data, IDictionary optionalRecordUnknown, StringFixedEnum fixedEnum, StringExtensibleEnum extensibleEnum, IntFixedEnum? optionalFixedEnum, IntExtensibleEnum? optionalExtensibleEnum, string label, int daysUsed, IList fooItems, XmlNestedModel anotherModel, IList modelsWithNamespaces, IList unwrappedModelsWithNamespaces, IList> listOfListFoo, IDictionary dictionaryFoo, IDictionary> dictionaryOfDictionaryFoo, IDictionary> dictionaryListFoo, IList> listOfDictionaryFoo) { Name = name; Age = age; @@ -201,7 +197,6 @@ internal XmlAdvancedModel(string name, int age, bool enabled, float score, strin DictionaryOfDictionaryFoo = dictionaryOfDictionaryFoo; DictionaryListFoo = dictionaryListFoo; ListOfDictionaryFoo = listOfDictionaryFoo; - _additionalBinaryDataProperties = additionalBinaryDataProperties; } /// A simple string property. diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlItem.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlItem.Serialization.cs index fb1ac232c86..ca698e6c7f3 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlItem.Serialization.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlItem.Serialization.cs @@ -7,7 +7,6 @@ using System; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.IO; using System.Xml; using System.Xml.Linq; @@ -127,7 +126,6 @@ internal static XmlItem DeserializeXmlItem(XElement element, ModelReaderWriterOp string itemName = default; int itemValue = default; string itemId = default; - IDictionary additionalBinaryDataProperties = new ChangeTrackingDictionary(); foreach (var attr in element.Attributes()) { @@ -153,7 +151,7 @@ internal static XmlItem DeserializeXmlItem(XElement element, ModelReaderWriterOp continue; } } - return new XmlItem(itemName, itemValue, itemId, additionalBinaryDataProperties); + return new XmlItem(itemName, itemValue, itemId); } } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlItem.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlItem.cs index 0788e17adf2..2bb9065a28e 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlItem.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlItem.cs @@ -6,16 +6,12 @@ #nullable disable using System; -using System.Collections.Generic; namespace SampleTypeSpec { /// An item model for XML array testing. public partial class XmlItem { - /// Keeps track of any properties unknown to the library. - private protected readonly IDictionary _additionalBinaryDataProperties; - /// Initializes a new instance of . /// The item name. /// The item value. @@ -31,19 +27,6 @@ public XmlItem(string itemName, int itemValue, string itemId) ItemId = itemId; } - /// Initializes a new instance of . - /// The item name. - /// The item value. - /// Item ID as attribute. - /// Keeps track of any properties unknown to the library. - internal XmlItem(string itemName, int itemValue, string itemId, IDictionary additionalBinaryDataProperties) - { - ItemName = itemName; - ItemValue = itemValue; - ItemId = itemId; - _additionalBinaryDataProperties = additionalBinaryDataProperties; - } - /// The item name. public string ItemName { get; set; } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlModelWithNamespace.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlModelWithNamespace.Serialization.cs index 33afc32982a..1ae8b077d2f 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlModelWithNamespace.Serialization.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlModelWithNamespace.Serialization.cs @@ -7,7 +7,6 @@ using System; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.IO; using System.Xml; using System.Xml.Linq; @@ -119,7 +118,6 @@ internal static XmlModelWithNamespace DeserializeXmlModelWithNamespace(XElement } string foo = default; - IDictionary additionalBinaryDataProperties = new ChangeTrackingDictionary(); foreach (var child in element.Elements()) { @@ -130,7 +128,7 @@ internal static XmlModelWithNamespace DeserializeXmlModelWithNamespace(XElement continue; } } - return new XmlModelWithNamespace(foo, additionalBinaryDataProperties); + return new XmlModelWithNamespace(foo); } } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlModelWithNamespace.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlModelWithNamespace.cs index 0311112da10..fac1a39d7d1 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlModelWithNamespace.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlModelWithNamespace.cs @@ -6,16 +6,12 @@ #nullable disable using System; -using System.Collections.Generic; namespace SampleTypeSpec { /// The XmlModelWithNamespace. public partial class XmlModelWithNamespace { - /// Keeps track of any properties unknown to the library. - private protected readonly IDictionary _additionalBinaryDataProperties; - /// Initializes a new instance of . /// /// is null. @@ -26,15 +22,6 @@ public XmlModelWithNamespace(string foo) Foo = foo; } - /// Initializes a new instance of . - /// - /// Keeps track of any properties unknown to the library. - internal XmlModelWithNamespace(string foo, IDictionary additionalBinaryDataProperties) - { - Foo = foo; - _additionalBinaryDataProperties = additionalBinaryDataProperties; - } - /// Gets or sets the Foo. public string Foo { get; set; } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlNestedModel.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlNestedModel.Serialization.cs index d0508de1e74..d10aaedba84 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlNestedModel.Serialization.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlNestedModel.Serialization.cs @@ -7,7 +7,6 @@ using System; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.IO; using System.Xml; using System.Xml.Linq; @@ -123,7 +122,6 @@ internal static XmlNestedModel DeserializeXmlNestedModel(XElement element, Model string value = default; int nestedId = default; - IDictionary additionalBinaryDataProperties = new ChangeTrackingDictionary(); foreach (var attr in element.Attributes()) { @@ -144,7 +142,7 @@ internal static XmlNestedModel DeserializeXmlNestedModel(XElement element, Model continue; } } - return new XmlNestedModel(value, nestedId, additionalBinaryDataProperties); + return new XmlNestedModel(value, nestedId); } } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlNestedModel.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlNestedModel.cs index 3ef8d208de6..2790a060a44 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlNestedModel.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlNestedModel.cs @@ -6,16 +6,12 @@ #nullable disable using System; -using System.Collections.Generic; namespace SampleTypeSpec { /// A nested model for XML testing. public partial class XmlNestedModel { - /// Keeps track of any properties unknown to the library. - private protected readonly IDictionary _additionalBinaryDataProperties; - /// Initializes a new instance of . /// The value of the nested model. /// An attribute on the nested model. @@ -28,17 +24,6 @@ public XmlNestedModel(string value, int nestedId) NestedId = nestedId; } - /// Initializes a new instance of . - /// The value of the nested model. - /// An attribute on the nested model. - /// Keeps track of any properties unknown to the library. - internal XmlNestedModel(string value, int nestedId, IDictionary additionalBinaryDataProperties) - { - Value = value; - NestedId = nestedId; - _additionalBinaryDataProperties = additionalBinaryDataProperties; - } - /// The value of the nested model. public string Value { get; set; } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/SampleTypeSpecModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/SampleTypeSpecModelFactory.cs index a0dc92df9f0..5552d775dbe 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/SampleTypeSpecModelFactory.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/SampleTypeSpecModelFactory.cs @@ -346,8 +346,7 @@ public static XmlAdvancedModel XmlAdvancedModel(string name = default, int age = dictionaryFoo, dictionaryOfDictionaryFoo, dictionaryListFoo, - listOfDictionaryFoo.ToList(), - additionalBinaryDataProperties: null); + listOfDictionaryFoo.ToList()); } /// An item model for XML array testing. @@ -357,7 +356,7 @@ public static XmlAdvancedModel XmlAdvancedModel(string name = default, int age = /// A new instance for mocking. public static XmlItem XmlItem(string itemName = default, int itemValue = default, string itemId = default) { - return new XmlItem(itemName, itemValue, itemId, additionalBinaryDataProperties: null); + return new XmlItem(itemName, itemValue, itemId); } /// A nested model for XML testing. @@ -366,7 +365,7 @@ public static XmlItem XmlItem(string itemName = default, int itemValue = default /// A new instance for mocking. public static XmlNestedModel XmlNestedModel(string value = default, int nestedId = default) { - return new XmlNestedModel(value, nestedId, additionalBinaryDataProperties: null); + return new XmlNestedModel(value, nestedId); } /// The XmlModelWithNamespace. @@ -374,7 +373,7 @@ public static XmlNestedModel XmlNestedModel(string value = default, int nestedId /// A new instance for mocking. public static XmlModelWithNamespace XmlModelWithNamespace(string foo = default) { - return new XmlModelWithNamespace(foo, additionalBinaryDataProperties: null); + return new XmlModelWithNamespace(foo); } /// diff --git a/packages/http-client-csharp/generator/TestProjects/Spector.Tests/Http/Payload/Xml/XmlTests.cs b/packages/http-client-csharp/generator/TestProjects/Spector.Tests/Http/Payload/Xml/XmlTests.cs index bf045267f4a..f5fb7e0aad3 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector.Tests/Http/Payload/Xml/XmlTests.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector.Tests/Http/Payload/Xml/XmlTests.cs @@ -390,6 +390,18 @@ public Task GetModelWithDatetime() => Test(async (host) => Assert.AreEqual(DateTimeOffset.Parse("Fri, 26 Aug 2022 14:38:00 GMT"), model.Rfc7231); }); + [SpectorTest] + public Task PutModelWithDatetime() => Test(async (host) => + { + var model = new ModelWithDatetime( + DateTimeOffset.Parse("2022-08-26T18:38:00Z"), + DateTimeOffset.Parse("Fri, 26 Aug 2022 14:38:00 GMT")); + var response = await new XmlClient(host, null).GetModelWithDatetimeValueClient() + .PutAsync(model); + + Assert.AreEqual(204, response.GetRawResponse().Status); + }); + [SpectorTest] public Task GetXmlErrorValue() => Test((host) => { diff --git a/packages/http-client-csharp/generator/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md index 89eb0e3cc88..e39281c3e78 100644 --- a/packages/http-client-csharp/generator/docs/backward-compatibility.md +++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md @@ -6,6 +6,10 @@ - [How It Works](#how-it-works) - [Supported Scenarios](#supported-scenarios) - [Model Factory Methods](#model-factory-methods) + - [New Model Property Added](#scenario-new-model-property-added) + - [Parameter Ordering Changed](#scenario-parameter-ordering-changed) + - [Property Renamed](#scenario-property-renamed) + - [New Property Added Together with a Rename](#scenario-new-property-added-together-with-a-rename) - [Model Properties](#model-properties) - [AdditionalProperties Type Preservation](#additionalproperties-type-preservation) - [API Version Enum](#api-version-enum) @@ -14,6 +18,7 @@ - [Parameter Naming](#parameter-naming) - [Page Size Parameter Casing Correction](#scenario-page-size-parameter-casing-correction) - [Top Parameter Conversion to MaxCount](#scenario-top-parameter-conversion-to-maxcount) + - [Method Parameter Name Preserved from Last Contract](#scenario-method-parameter-name-preserved-from-last-contract) - [Content-Type Parameter Ordering](#content-type-parameter-ordering) - [Content-Type Before Body Preserved from Last Contract](#scenario-content-type-before-body-preserved-from-last-contract) @@ -110,6 +115,105 @@ public static PublicModel1 PublicModel1( **Result:** The generator keeps the previous parameter ordering to maintain compatibility. +#### Scenario: Property Renamed + +**Description:** When a model property is renamed (via `@@clientName`, a spec rename, a generator naming-rule change, etc.), the generated factory parameter would normally change its name to follow the new property name. Renaming a parameter is source-breaking for callers using named arguments and is not flagged by ApiCompat / binary-compat tooling. To avoid this, the generator preserves the previous parameter name on the current factory method whenever the only difference between the previous and current method is one or more parameter names (same method name, same parameter types in the same order, same parameter count). + +**Example:** + +Previous version exposed `stringProp` and `modelProp`: + +```csharp +public static PublicModel1 PublicModel1( + string stringProp = default, + Thing modelProp = default, + IEnumerable listProp = default, + IDictionary dictProp = default) +``` + +Current TypeSpec renames the underlying properties to `CertificateStringProp` and `CertificateModelProp`, which would normally produce: + +```csharp +public static PublicModel1 PublicModel1( + string certificateStringProp = default, + Thing certificateModelProp = default, + IEnumerable listProp = default, + IDictionary dictProp = default) +``` + +**Generated Compatibility Result:** + +The generator detects that previous and current methods differ only in parameter names and preserves the previous names on the current method: + +```csharp +public static PublicModel1 PublicModel1( + string stringProp = default, + Thing modelProp = default, + IEnumerable listProp = default, + IDictionary dictProp = default) +{ + // body uses the preserved names when constructing the model + return new PublicModel1(stringProp, modelProp, listProp.ToList(), dictProp, additionalBinaryDataProperties: null); +} +``` + +**Key Points:** + +- Only one method is generated — no additional `[EditorBrowsable(EditorBrowsableState.Never)]` overload is needed +- Existing source code using named arguments (e.g. `PublicModel1(stringProp: "x")`) continues to compile +- The matching is by method name plus parameter types in the same order; the parameter count must also match. If a parameter is added or removed in addition to a rename, the standard "new property added" overload is generated instead (see scenario below) +- The XML doc `` entries on the method are updated to reference the preserved names + +#### Scenario: New Property Added Together with a Rename + +**Description:** When a new property is added to a model AND a previously-published property has been renamed at the same time, the parameter counts of the previous and current factory methods differ, so the rename-only fast path above does not apply. The generator falls back to the standard "new property added" flow and emits an `[EditorBrowsable(EditorBrowsableState.Never)]` backcompat overload using the previously-published parameter names. + +> [!NOTE] +> The body of the backcompat overload constructs the model directly. Renamed parameters whose names no longer match a current property are passed as `default` to the constructor, since the body can no longer thread them through the renamed properties. This keeps existing source compiling, but callers who depended on the old parameter being forwarded should migrate to the current method. + +**Example:** + +Previous version exposed three properties, with the first two under different names: + +```csharp +public static PublicModel1 PublicModel1( + string oldStringProp = default, + Thing oldModelProp = default, + IEnumerable listProp = default) +``` + +Current TypeSpec renames the first two properties (now `stringProp` / `modelProp`) and adds a new property (`dictProp`): + +```csharp +public static PublicModel1 PublicModel1( + string stringProp = default, + Thing modelProp = default, + IEnumerable listProp = default, + IDictionary dictProp = default) +``` + +**Generated Compatibility Method:** + +```csharp +[EditorBrowsable(EditorBrowsableState.Never)] +public static PublicModel1 PublicModel1( + string oldStringProp, + Thing oldModelProp, + IEnumerable listProp) +{ + listProp ??= new ChangeTrackingList(); + + return new PublicModel1(default, default, listProp.ToList(), default, additionalBinaryDataProperties: null); +} +``` + +**Key Points:** + +- The previous (renamed) signature is preserved so existing source code continues to compile +- Parameters whose names matched a current property (`listProp`) are threaded through; renamed parameters are passed as `default` +- The new property is also passed as `default` +- A separate visible method exposes the current signature with the new property and the renamed parameters + ### Model Properties The generator preserves the previous property type whenever it differs from the type produced by the current spec. This applies to all public model properties (scalars, enums, models, and collections), so any property type change is non-source-breaking by default. Users who want the new spec's type to take effect can override this behavior with custom code. @@ -613,6 +717,50 @@ public virtual AsyncPageable GetItemsAsync(int? maxCount = null, Cancellat - Existing client code with `top` continues to compile without changes - New code benefits from the standardized `maxCount` naming convention +#### Scenario: Method Parameter Name Preserved from Last Contract + +**Description:** When a service method parameter is renamed by the spec or by the generator (e.g., a `@@clientName`, a TypeSpec property rename, or a generator naming-rule change), the new name would normally appear on the generated convenience and protocol methods. Renaming a parameter is source-breaking for callers using named arguments and is not flagged by ApiCompat / binary-compat tooling. To avoid this, the generator looks up the parameter's original (spec) name in `LastContractView` and, when a previously-published parameter with that name exists on the matching method (matched by method name, allowing for the sync/async pair), restores the previously-published parameter name on the current method. + +This generalizes the paging-specific `top → maxCount` and page-size casing scenarios above so that any renamed parameter on any operation falls back to the prior published name. + +**Example:** + +Previous version exposed `oldParam` on `GetSomething`: + +```csharp +public virtual ClientResult GetSomething(string oldParam, RequestOptions options = null) +{ + // ... +} +``` + +Current TypeSpec renames the parameter to `newParam` (e.g., via `@@clientName` or a property rename), which would normally produce: + +```csharp +public virtual ClientResult GetSomething(string newParam, RequestOptions options = null) +{ + // ... +} +``` + +**Generated Compatibility Result:** + +The generator detects `oldParam` on the matching method in `LastContractView` and restores that name on the current method: + +```csharp +public virtual ClientResult GetSomething(string oldParam, RequestOptions options = null) +{ + // body and HTTP request still use the spec's serialized name; only the public parameter name is preserved +} +``` + +**Key Points:** + +- Lookup is scoped to the matching service method (allowing for the sync/async pair) so a parameter name shared across multiple methods cannot false-match another method's parameter +- The HTTP query/path/header/body serialized name continues to use the spec's wire name — only the C# parameter identifier is restored +- Existing source code using named arguments (e.g., `client.GetSomething(oldParam: "x")`) continues to compile +- If no matching parameter is found in `LastContractView`, the generator uses the current (renamed) name + ### Content-Type Parameter Ordering The generator places the `contentType` parameter after the body (`content`) parameter in method signatures. However, backward compatibility is maintained when the last contract had a different ordering. diff --git a/packages/http-client-csharp/package.json b/packages/http-client-csharp/package.json index df2dbb30a42..b1388c45a5d 100644 --- a/packages/http-client-csharp/package.json +++ b/packages/http-client-csharp/package.json @@ -25,11 +25,16 @@ "default": "./dist/emitter/src/index.js" } }, + "browser": { + "./dist/emitter/src/emit-generate.js": "./dist/emitter/src/emit-generate.browser.js", + "./emitter/src/emit-generate.ts": "./emitter/src/emit-generate.browser.ts" + }, "scripts": { "clean": "rimraf ./dist ./emitter/temp && dotnet clean ./generator", "build:emitter": "tsc -p ./emitter/tsconfig.build.json", "build:generator": "dotnet build ./generator", "build": "npm run build:emitter && npm run build:generator && npm run extract-api", + "dev:playground": "npm run build:emitter && cd ../../website && npm run dev", "gen-extern-signature": "tspd --enable-experimental gen-extern-signature .", "watch": "tsc -p ./emitter/tsconfig.build.json --watch", "lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", diff --git a/packages/http-client-csharp/playground-server/.gitignore b/packages/http-client-csharp/playground-server/.gitignore new file mode 100644 index 00000000000..cd42ee34e87 --- /dev/null +++ b/packages/http-client-csharp/playground-server/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ diff --git a/packages/http-client-csharp/playground-server/Dockerfile b/packages/http-client-csharp/playground-server/Dockerfile new file mode 100644 index 00000000000..953ffae59d5 --- /dev/null +++ b/packages/http-client-csharp/playground-server/Dockerfile @@ -0,0 +1,29 @@ +# Build from the http-client-csharp package root: +# docker build -f playground-server/Dockerfile -t csharp-playground-server . +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src + +# Build the generator (populates dist/generator/) +COPY generator/ generator/ +COPY eng/ eng/ +RUN dotnet build generator -c Release + +# Build the server +COPY playground-server/playground-server.csproj playground-server/ +RUN dotnet restore playground-server/playground-server.csproj +COPY playground-server/ playground-server/ +RUN dotnet publish playground-server -c Release -o /app --no-restore + +# Copy generator output +RUN cp -r dist/generator /app/generator + +# Need full SDK (not just aspnet) because the server spawns `dotnet` to run the generator DLL +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime +WORKDIR /app +COPY --from=build /app . + +ENV DOTNET_ENVIRONMENT=Production +ENV GENERATOR_PATH=/app/generator/Microsoft.TypeSpec.Generator.dll + +EXPOSE 5174 +ENTRYPOINT ["dotnet", "playground-server.dll"] diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs new file mode 100644 index 00000000000..ea0b84df78c --- /dev/null +++ b/packages/http-client-csharp/playground-server/Program.cs @@ -0,0 +1,263 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.RateLimiting; +using Microsoft.AspNetCore.RateLimiting; + +const int MaxRequestBodySize = 10 * 1024 * 1024; // 10 MB +const int GeneratorTimeoutSeconds = 300; + +var builder = WebApplication.CreateBuilder(args); + +var allowedOrigins = new HashSet(StringComparer.OrdinalIgnoreCase) +{ + "http://localhost:5173", // vite dev + "http://localhost:4173", // vite preview + "http://localhost:3000", + "https://typespec.io", + "https://www.typespec.io", + "https://azure.github.io", +}; +// Add additional origins from PLAYGROUND_URLS (comma-separated) or PLAYGROUND_URL (single) +var playgroundUrls = Environment.GetEnvironmentVariable("PLAYGROUND_URLS") + ?? Environment.GetEnvironmentVariable("PLAYGROUND_URL"); +if (!string.IsNullOrEmpty(playgroundUrls)) +{ + foreach (var origin in playgroundUrls.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + if (Uri.TryCreate(origin, UriKind.Absolute, out var uri)) + { + allowedOrigins.Add(uri.GetLeftPart(UriPartial.Authority)); + } + } +} + +builder.Services.AddCors(); +builder.Services.AddRateLimiter(options => +{ + options.RejectionStatusCode = 429; + options.AddFixedWindowLimiter("generate", limiter => + { + limiter.PermitLimit = 10; + limiter.Window = TimeSpan.FromMinutes(1); + limiter.QueueLimit = 2; + limiter.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; + }); +}); + +// Limit request body size +builder.WebHost.ConfigureKestrel(options => +{ + options.Limits.MaxRequestBodySize = MaxRequestBodySize; +}); + +var app = builder.Build(); + +// Security headers +app.Use(async (context, next) => +{ + context.Response.Headers["X-Content-Type-Options"] = "nosniff"; + context.Response.Headers["X-Frame-Options"] = "DENY"; + context.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin"; + await next(); +}); + +app.UseCors(policy => policy + .WithOrigins([.. allowedOrigins]) + .AllowAnyMethod() + .AllowAnyHeader()); + +app.UseRateLimiter(); + +// Resolve the generator DLL path. Default: dist/generator in the http-client-csharp package. +var generatorPath = Environment.GetEnvironmentVariable("GENERATOR_PATH") + ?? Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "dist", "generator", "Microsoft.TypeSpec.Generator.dll")); + +if (!File.Exists(generatorPath)) +{ + Console.Error.WriteLine($"WARNING: Generator DLL not found at {generatorPath}"); + Console.Error.WriteLine("Set GENERATOR_PATH environment variable to the correct path."); +} +else +{ + Console.WriteLine($"Generator DLL: {generatorPath}"); +} + +app.MapGet("/health", () => +{ + string dotnetVersion; + try + { + var psi = new ProcessStartInfo("dotnet", "--version") { RedirectStandardOutput = true, UseShellExecute = false }; + var proc = Process.Start(psi)!; + dotnetVersion = proc.StandardOutput.ReadToEnd().Trim(); + proc.WaitForExit(); + } + catch (Exception ex) { dotnetVersion = ex.Message; } + + return Results.Ok(new + { + status = "ok", + generatorFound = File.Exists(generatorPath), + generatorPath, + dotnetVersion, + runtime = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription, + os = System.Runtime.InteropServices.RuntimeInformation.OSDescription, + arch = System.Runtime.InteropServices.RuntimeInformation.OSArchitecture.ToString() + }); +}); + +app.MapPost("/generate", async (HttpRequest request) => +{ + // Validate content type + if (!request.ContentType?.StartsWith("application/json", StringComparison.OrdinalIgnoreCase) ?? true) + { + return Results.BadRequest(new { error = "Content-Type must be application/json" }); + } + + GenerateRequest? body; + try + { + body = await JsonSerializer.DeserializeAsync( + request.Body, GenerateJsonContext.Default.GenerateRequest); + } + catch (JsonException) + { + return Results.BadRequest(new { error = "Invalid JSON in request body" }); + } + + if (body?.CodeModel is null || body?.Configuration is null) + { + return Results.BadRequest(new { error = "Missing 'codeModel' or 'configuration' fields" }); + } + + var generatorName = body.GeneratorName ?? "ScmCodeModelGenerator"; + + if (!File.Exists(generatorPath)) + { + return Results.StatusCode(503); + } + + // Create a temporary working directory + var tempDir = Path.Combine(Path.GetTempPath(), "tsp-playground", Guid.NewGuid().ToString("N")); + var generatedDir = Path.Combine(tempDir, "src", "Generated"); + Directory.CreateDirectory(generatedDir); + + try + { + // Write the input files the generator expects + await File.WriteAllTextAsync(Path.Combine(tempDir, "tspCodeModel.json"), body.CodeModel); + await File.WriteAllTextAsync(Path.Combine(tempDir, "Configuration.json"), body.Configuration); + + // Run the .NET generator as a subprocess + Console.WriteLine($"Starting generator: dotnet --roll-forward Major {generatorPath} {tempDir} -g {generatorName} --new-project"); + Console.WriteLine($"Code model size: {body.CodeModel!.Length} chars"); + Console.WriteLine($"Configuration: {body.Configuration}"); + + var psi = new ProcessStartInfo + { + FileName = "dotnet", + ArgumentList = { "--roll-forward", "Major", generatorPath, tempDir, "-g", generatorName, "--new-project" }, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + using var process = Process.Start(psi)!; + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(GeneratorTimeoutSeconds)); + + // Stream stdout/stderr to console for logging + var stderrLines = new List(); + var stdoutTask = Task.Run(async () => + { + string? line; + while ((line = await process.StandardOutput.ReadLineAsync()) != null) + { + Console.WriteLine($"[generator stdout] {line}"); + } + }); + var stderrTask = Task.Run(async () => + { + string? line; + while ((line = await process.StandardError.ReadLineAsync()) != null) + { + Console.Error.WriteLine($"[generator stderr] {line}"); + stderrLines.Add(line); + } + }); + + try + { + await process.WaitForExitAsync(cts.Token); + } + catch (OperationCanceledException) + { + process.Kill(entireProcessTree: true); + return Results.Json( + new GenerateErrorResponse("Generator timed out", $"Process did not complete within {GeneratorTimeoutSeconds} seconds"), + GenerateJsonContext.Default.GenerateErrorResponse, + statusCode: 504); + } + await Task.WhenAll(stdoutTask, stderrTask); + + var exitCode = process.ExitCode; + Console.WriteLine($"Generator exited with code {exitCode}"); + + if (exitCode != 0) + { + return Results.Json( + new GenerateErrorResponse($"Generator failed with exit code {exitCode}", string.Join("\n", stderrLines.TakeLast(50))), + GenerateJsonContext.Default.GenerateErrorResponse, + statusCode: 500); + } + + // Collect all generated files + var files = new List(); + if (Directory.Exists(tempDir)) + { + foreach (var filePath in Directory.EnumerateFiles(tempDir, "*", SearchOption.AllDirectories)) + { + // Skip the input files + var fileName = Path.GetFileName(filePath); + if (fileName is "tspCodeModel.json" or "Configuration.json") + continue; + + var relativePath = Path.GetRelativePath(tempDir, filePath).Replace('\\', '/'); + var content = await File.ReadAllTextAsync(filePath); + files.Add(new GeneratedFile(relativePath, content)); + } + } + + return Results.Json( + new GenerateResponse(files), + GenerateJsonContext.Default.GenerateResponse); + } + finally + { + try { Directory.Delete(tempDir, recursive: true); } catch { } + } +}).RequireRateLimiting("generate"); + +var port = Environment.GetEnvironmentVariable("PORT") + ?? Environment.GetEnvironmentVariable("WEBSITES_PORT") + ?? "5174"; +var url = Environment.GetEnvironmentVariable("ASPNETCORE_URLS") ?? $"http://+:{port}"; +Console.WriteLine($"C# playground server listening on {url}"); +app.Run(url); + +// --- Request/Response types --- + +record GenerateRequest(string? CodeModel, string? Configuration, string? GeneratorName); +record GeneratedFile(string Path, string Content); +record GenerateResponse(List Files); +record GenerateErrorResponse(string Error, string? Details); + +[JsonSerializable(typeof(GenerateRequest))] +[JsonSerializable(typeof(GenerateResponse))] +[JsonSerializable(typeof(GenerateErrorResponse))] +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +partial class GenerateJsonContext : JsonSerializerContext { } diff --git a/packages/http-client-csharp/playground-server/playground-server.csproj b/packages/http-client-csharp/playground-server/playground-server.csproj new file mode 100644 index 00000000000..8c5ce456c81 --- /dev/null +++ b/packages/http-client-csharp/playground-server/playground-server.csproj @@ -0,0 +1,10 @@ + + + + net10.0 + enable + enable + PlaygroundServer + + + diff --git a/packages/http-client-java/generator/http-client-generator-clientcore-test/package.json b/packages/http-client-java/generator/http-client-generator-clientcore-test/package.json index a98cbb18efd..4b5325f38bb 100644 --- a/packages/http-client-java/generator/http-client-generator-clientcore-test/package.json +++ b/packages/http-client-java/generator/http-client-generator-clientcore-test/package.json @@ -29,8 +29,8 @@ "@typespec/events": "0.81.0", "@typespec/sse": "0.81.0", "@typespec/streams": "0.81.0", - "@azure-tools/typespec-azure-core": "0.67.0", - "@azure-tools/typespec-client-generator-core": "0.67.2", + "@azure-tools/typespec-azure-core": "0.67.1", + "@azure-tools/typespec-client-generator-core": "0.67.3", "@azure-tools/typespec-azure-resource-manager": "0.67.1", "@azure-tools/typespec-autorest": "0.67.0" }, diff --git a/packages/http-client-java/generator/http-client-generator-test/package.json b/packages/http-client-java/generator/http-client-generator-test/package.json index c2325eca1c9..51088217515 100644 --- a/packages/http-client-java/generator/http-client-generator-test/package.json +++ b/packages/http-client-java/generator/http-client-generator-test/package.json @@ -29,8 +29,8 @@ "@typespec/events": "0.81.0", "@typespec/sse": "0.81.0", "@typespec/streams": "0.81.0", - "@azure-tools/typespec-azure-core": "0.67.0", - "@azure-tools/typespec-client-generator-core": "0.67.2", + "@azure-tools/typespec-azure-core": "0.67.1", + "@azure-tools/typespec-client-generator-core": "0.67.3", "@azure-tools/typespec-azure-resource-manager": "0.67.1", "@azure-tools/typespec-autorest": "0.67.0" }, diff --git a/packages/http-client-java/package-lock.json b/packages/http-client-java/package-lock.json index da5d27651e7..84abb39defa 100644 --- a/packages/http-client-java/package-lock.json +++ b/packages/http-client-java/package-lock.json @@ -15,12 +15,12 @@ }, "devDependencies": { "@azure-tools/typespec-autorest": "0.67.0", - "@azure-tools/typespec-azure-core": "0.67.0", + "@azure-tools/typespec-azure-core": "0.67.1", "@azure-tools/typespec-azure-resource-manager": "0.67.1", "@azure-tools/typespec-azure-rulesets": "0.67.0", - "@azure-tools/typespec-client-generator-core": "0.67.2", - "@microsoft/api-extractor": "^7.58.2", - "@microsoft/api-extractor-model": "^7.33.6", + "@azure-tools/typespec-client-generator-core": "0.67.3", + "@microsoft/api-extractor": "^7.58.7", + "@microsoft/api-extractor-model": "^7.33.8", "@types/js-yaml": "~4.0.9", "@types/lodash": "~4.17.24", "@types/node": "~25.6.0", @@ -34,21 +34,21 @@ "@typespec/streams": "0.81.0", "@typespec/versioning": "0.81.0", "@typespec/xml": "0.81.0", - "@vitest/coverage-v8": "^4.1.4", - "@vitest/ui": "^4.1.4", + "@vitest/coverage-v8": "^4.1.5", + "@vitest/ui": "^4.1.5", "c8": "~11.0.0", "rimraf": "~6.1.3", - "typescript": "~6.0.2", - "vitest": "^4.1.4" + "typescript": "~6.0.3", + "vitest": "^4.1.5" }, "engines": { "node": ">=20.0.0" }, "peerDependencies": { "@azure-tools/typespec-autorest": ">=0.67.0 <1.0.0", - "@azure-tools/typespec-azure-core": ">=0.67.0 <1.0.0", + "@azure-tools/typespec-azure-core": ">=0.67.1 <1.0.0", "@azure-tools/typespec-azure-resource-manager": ">=0.67.1 <1.0.0", - "@azure-tools/typespec-client-generator-core": ">=0.67.2 <1.0.0", + "@azure-tools/typespec-client-generator-core": ">=0.67.3 <1.0.0", "@typespec/compiler": "^1.11.0", "@typespec/events": ">=0.81.0 <1.0.0", "@typespec/http": "^1.11.0", @@ -136,9 +136,9 @@ } }, "node_modules/@azure-tools/typespec-azure-core": { - "version": "0.67.0", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-azure-core/-/typespec-azure-core-0.67.0.tgz", - "integrity": "sha512-6DO/fOlVihMlPG0oDXrgURf5MNF4iBzPx5SMA5aaFDx/fW6MjiD+TN9Yy9O+l9mVNh1XaEMjhjA8/lmnHZ/U0g==", + "version": "0.67.1", + "resolved": "https://registry.npmjs.org/@azure-tools/typespec-azure-core/-/typespec-azure-core-0.67.1.tgz", + "integrity": "sha512-HBvigwr8Ub7rsg4RDpTO3WTHS+CIqAw32X3RzxsDNb8NfLoSLSZDANz05VPiDTzAXO7eMfEP42RXkKPV0tlZLg==", "dev": true, "license": "MIT", "engines": { @@ -189,9 +189,9 @@ } }, "node_modules/@azure-tools/typespec-client-generator-core": { - "version": "0.67.2", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-client-generator-core/-/typespec-client-generator-core-0.67.2.tgz", - "integrity": "sha512-sI2Lw7F6aTXWNrLGQU6fjyx4H3ztq/Y3VKOUfe4nNQY664NgrBA6B8e0Wa4wVTEXDs9BeZZYMgzTeAIl5eu6sg==", + "version": "0.67.3", + "resolved": "https://registry.npmjs.org/@azure-tools/typespec-client-generator-core/-/typespec-client-generator-core-0.67.3.tgz", + "integrity": "sha512-hK+1juRH2kWPCVfqsVYKFw8RbhLJY4ioh6OTZA4JmPXwQ4zz0/nsaGHG6K+tZ9D7yRyYd06GNxJoAbWabFfDYA==", "dev": true, "license": "MIT", "dependencies": { @@ -203,7 +203,7 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@azure-tools/typespec-azure-core": "^0.67.0", + "@azure-tools/typespec-azure-core": "^0.67.1", "@typespec/compiler": "^1.11.0", "@typespec/events": "^0.81.0", "@typespec/http": "^1.11.0", @@ -995,24 +995,23 @@ } }, "node_modules/@microsoft/api-extractor": { - "version": "7.58.2", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.58.2.tgz", - "integrity": "sha512-qmqWa0Fx1xn3irQy8MyuAKUs8e3CdwMJOujaPkM8gx5v/V7RcLhTjBU0/uL2kdhmROpW+5WG1FD98O441kkvQQ==", + "version": "7.58.7", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.58.7.tgz", + "integrity": "sha512-yK6OycD46gIzLRpj6ueVUWPk1ACSpkN1LBo05gY1qPTylbWyUCanXfH7+VgkI5LJrJoRSQR5F04XuCffCXLOBw==", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/api-extractor-model": "7.33.6", + "@microsoft/api-extractor-model": "7.33.8", "@microsoft/tsdoc": "~0.16.0", "@microsoft/tsdoc-config": "~0.18.1", - "@rushstack/node-core-library": "5.22.0", - "@rushstack/rig-package": "0.7.2", - "@rushstack/terminal": "0.22.5", - "@rushstack/ts-command-line": "5.3.5", + "@rushstack/node-core-library": "5.23.1", + "@rushstack/rig-package": "0.7.3", + "@rushstack/terminal": "0.24.0", + "@rushstack/ts-command-line": "5.3.9", "diff": "~8.0.2", - "lodash": "~4.18.1", "minimatch": "10.2.3", "resolve": "~1.22.1", - "semver": "~7.5.4", + "semver": "~7.7.4", "source-map": "~0.6.1", "typescript": "5.9.3" }, @@ -1021,42 +1020,15 @@ } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.33.6", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.33.6.tgz", - "integrity": "sha512-E9iI4yGEVVusbTAqSLetVFxDuBVCVqCigcoQwdJuOjsLq5Hry3MkBgUQhSZNzLCu17pgjk58MI80GRDJLht/1A==", + "version": "7.33.8", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.33.8.tgz", + "integrity": "sha512-aIcoQggPyer3B6Ze3usz0YWC/oBwUHfRH5ETUsr+oT2BRA6SfTJl7IKPcPZkX4UR+PohowzW4uMxsvjrn8vm+w==", "dev": true, "license": "MIT", "dependencies": { "@microsoft/tsdoc": "~0.16.0", "@microsoft/tsdoc-config": "~0.18.1", - "@rushstack/node-core-library": "5.22.0" - } - }, - "node_modules/@microsoft/api-extractor/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@microsoft/api-extractor/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "@rushstack/node-core-library": "5.23.1" } }, "node_modules/@microsoft/api-extractor/node_modules/typescript": { @@ -1094,9 +1066,9 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", - "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", "optional": true, @@ -1151,9 +1123,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.124.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", - "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "version": "0.126.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz", + "integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==", "dev": true, "license": "MIT", "funding": { @@ -1168,9 +1140,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz", + "integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==", "cpu": [ "arm64" ], @@ -1185,9 +1157,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz", + "integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==", "cpu": [ "arm64" ], @@ -1202,9 +1174,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", - "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz", + "integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==", "cpu": [ "x64" ], @@ -1219,9 +1191,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", - "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz", + "integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==", "cpu": [ "x64" ], @@ -1236,9 +1208,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", - "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz", + "integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==", "cpu": [ "arm" ], @@ -1253,9 +1225,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz", + "integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==", "cpu": [ "arm64" ], @@ -1273,9 +1245,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", - "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz", + "integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==", "cpu": [ "arm64" ], @@ -1293,9 +1265,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz", + "integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==", "cpu": [ "ppc64" ], @@ -1313,9 +1285,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz", + "integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==", "cpu": [ "s390x" ], @@ -1333,9 +1305,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz", + "integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==", "cpu": [ "x64" ], @@ -1353,9 +1325,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", - "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz", + "integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==", "cpu": [ "x64" ], @@ -1373,9 +1345,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz", + "integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==", "cpu": [ "arm64" ], @@ -1390,9 +1362,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", - "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz", + "integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==", "cpu": [ "wasm32" ], @@ -1402,16 +1374,16 @@ "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", - "@napi-rs/wasm-runtime": "^1.1.3" + "@napi-rs/wasm-runtime": "^1.1.4" }, "engines": { - "node": ">=14.0.0" + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", - "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz", + "integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==", "cpu": [ "arm64" ], @@ -1426,9 +1398,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", - "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz", + "integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==", "cpu": [ "x64" ], @@ -1443,16 +1415,16 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", - "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz", + "integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==", "dev": true, "license": "MIT" }, "node_modules/@rushstack/node-core-library": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.22.0.tgz", - "integrity": "sha512-S/Dm/N+8tkbasS6yM5cF6q4iDFt14mQQniiVIwk1fd0zpPwWESspO4qtPyIl8szEaN86XOYC1HRRzZrOowxjtw==", + "version": "5.23.1", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.23.1.tgz", + "integrity": "sha512-wlKmIKIYCKuCASbITvOxLZXepPbwXvrv7S6ig6XNWFchSyhL/E2txmVXspHY49Wu2dzf7nI27a2k/yV5BA3EiA==", "dev": true, "license": "MIT", "dependencies": { @@ -1463,7 +1435,7 @@ "import-lazy": "~4.0.0", "jju": "~1.4.0", "resolve": "~1.22.1", - "semver": "~7.5.4" + "semver": "~7.7.4" }, "peerDependencies": { "@types/node": "*" @@ -1474,35 +1446,6 @@ } } }, - "node_modules/@rushstack/node-core-library/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@rushstack/node-core-library/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@rushstack/problem-matcher": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@rushstack/problem-matcher/-/problem-matcher-0.2.1.tgz", @@ -1519,24 +1462,24 @@ } }, "node_modules/@rushstack/rig-package": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.7.2.tgz", - "integrity": "sha512-9XbFWuqMYcHUso4mnETfhGVUSaADBRj6HUAAEYk50nMPn8WRICmBuCphycQGNB3duIR6EEZX3Xj3SYc2XiP+9A==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.7.3.tgz", + "integrity": "sha512-aAA518n6wxxjCfnTAOjQnm7ngNE0FVHxHAw2pxKlIhxrMn0XQjGcXKF0oKWpjBgJOmsaJpVob/v+zr3zxgPWuA==", "dev": true, "license": "MIT", "dependencies": { - "resolve": "~1.22.1", - "strip-json-comments": "~3.1.1" + "jju": "~1.4.0", + "resolve": "~1.22.1" } }, "node_modules/@rushstack/terminal": { - "version": "0.22.5", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.22.5.tgz", - "integrity": "sha512-umej8J6A+WRbfQV1G/uNfnz4bMa8CzFU9IJzQb/ZcH4j7Ybg3BQ8UBKOCF3o5U3/2yah1TDU/zE71ugg2JJv+Q==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.24.0.tgz", + "integrity": "sha512-8ZQS4MMaGsv27EXCBiH7WMPkRZrffeDoIevs6z9TM5dzqiY6+Hn4evfK/G+gvgBTjfvfkHIZPQQmalmI2sM4TQ==", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/node-core-library": "5.22.0", + "@rushstack/node-core-library": "5.23.1", "@rushstack/problem-matcher": "0.2.1", "supports-color": "~8.1.1" }, @@ -1550,13 +1493,13 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.3.5.tgz", - "integrity": "sha512-ToJQu3+o6aEdDoApGrwb/RsbwDi/NSC7jIEaAezzWM470TRrsXfSHoYAm1eWkhh34xJ+kZxU1ZzKSHiOMlOFPA==", + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.3.9.tgz", + "integrity": "sha512-GIHqU+sRGQ3LGWAZu1O+9Yh++qwtyNIIGuNbcWHJjBTm2qRez0cwINUHZ+pQLR8UuzZDcMajrDaNbUYoaL/XtQ==", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/terminal": "0.22.5", + "@rushstack/terminal": "0.24.0", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" @@ -2175,14 +2118,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.4.tgz", - "integrity": "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.5.tgz", + "integrity": "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.4", + "@vitest/utils": "4.1.5", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -2196,8 +2139,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.4", - "vitest": "4.1.4" + "@vitest/browser": "4.1.5", + "vitest": "4.1.5" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2206,16 +2149,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", - "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", + "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -2224,13 +2167,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", - "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", + "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.4", + "@vitest/spy": "4.1.5", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2251,9 +2194,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", - "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", + "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", "dev": true, "license": "MIT", "dependencies": { @@ -2264,13 +2207,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", - "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", + "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.4", + "@vitest/utils": "4.1.5", "pathe": "^2.0.3" }, "funding": { @@ -2278,14 +2221,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", - "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", + "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/pretty-format": "4.1.5", + "@vitest/utils": "4.1.5", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2294,9 +2237,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", - "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", + "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", "dev": true, "license": "MIT", "funding": { @@ -2304,13 +2247,13 @@ } }, "node_modules/@vitest/ui": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.4.tgz", - "integrity": "sha512-EgFR7nlj5iTDYZYCvavjFokNYwr3c3ry0sFiCg+N7B233Nwp+NNx7eoF/XvMWDCKY71xXAG3kFkt97ZHBJVL8A==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.5.tgz", + "integrity": "sha512-3Z9HNFiV0IF1fk0JPiK+7kE1GcaIPefQQIBYur6PM5yFIq6agys3uqP/0t966e1wXfmjbRCHDe7qW236Xjwnag==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.4", + "@vitest/utils": "4.1.5", "fflate": "^0.8.2", "flatted": "^3.4.2", "pathe": "^2.0.3", @@ -2322,17 +2265,17 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "4.1.4" + "vitest": "4.1.5" } }, "node_modules/@vitest/utils": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", - "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", + "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.4", + "@vitest/pretty-format": "4.1.5", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -4367,9 +4310,9 @@ "dev": true }, "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5405,9 +5348,9 @@ } }, "node_modules/postcss": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", - "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "dev": true, "funding": [ { @@ -5594,12 +5537,13 @@ } }, "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "dev": true, "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" @@ -5655,14 +5599,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", - "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz", + "integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.124.0", - "@rolldown/pluginutils": "1.0.0-rc.15" + "@oxc-project/types": "=0.126.0", + "@rolldown/pluginutils": "1.0.0-rc.16" }, "bin": { "rolldown": "bin/cli.mjs" @@ -5671,21 +5615,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.15", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", - "@rolldown/binding-darwin-x64": "1.0.0-rc.15", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + "@rolldown/binding-android-arm64": "1.0.0-rc.16", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.16", + "@rolldown/binding-darwin-x64": "1.0.0-rc.16", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.16", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.16", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.16", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.16", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16" } }, "node_modules/router": { @@ -6099,9 +6043,9 @@ } }, "node_modules/std-env": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", - "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, @@ -6174,19 +6118,6 @@ "node": ">=8" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strnum": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", @@ -6442,9 +6373,9 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", - "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6537,17 +6468,17 @@ } }, "node_modules/vite": { - "version": "8.0.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", - "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "version": "8.0.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz", + "integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.15", - "tinyglobby": "^0.2.15" + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.16", + "tinyglobby": "^0.2.16" }, "bin": { "vite": "bin/vite.js" @@ -6628,19 +6559,19 @@ } }, "node_modules/vitest": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", - "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", + "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.4", - "@vitest/mocker": "4.1.4", - "@vitest/pretty-format": "4.1.4", - "@vitest/runner": "4.1.4", - "@vitest/snapshot": "4.1.4", - "@vitest/spy": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/expect": "4.1.5", + "@vitest/mocker": "4.1.5", + "@vitest/pretty-format": "4.1.5", + "@vitest/runner": "4.1.5", + "@vitest/snapshot": "4.1.5", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -6668,12 +6599,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.4", - "@vitest/browser-preview": "4.1.4", - "@vitest/browser-webdriverio": "4.1.4", - "@vitest/coverage-istanbul": "4.1.4", - "@vitest/coverage-v8": "4.1.4", - "@vitest/ui": "4.1.4", + "@vitest/browser-playwright": "4.1.5", + "@vitest/browser-preview": "4.1.5", + "@vitest/browser-webdriverio": "4.1.5", + "@vitest/coverage-istanbul": "4.1.5", + "@vitest/coverage-v8": "4.1.5", + "@vitest/ui": "4.1.5", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -6940,12 +6871,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/yaml": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", diff --git a/packages/http-client-java/package.json b/packages/http-client-java/package.json index 62c20e5924d..06873fccaae 100644 --- a/packages/http-client-java/package.json +++ b/packages/http-client-java/package.json @@ -50,9 +50,9 @@ ], "peerDependencies": { "@azure-tools/typespec-autorest": ">=0.67.0 <1.0.0", - "@azure-tools/typespec-azure-core": ">=0.67.0 <1.0.0", + "@azure-tools/typespec-azure-core": ">=0.67.1 <1.0.0", "@azure-tools/typespec-azure-resource-manager": ">=0.67.1 <1.0.0", - "@azure-tools/typespec-client-generator-core": ">=0.67.2 <1.0.0", + "@azure-tools/typespec-client-generator-core": ">=0.67.3 <1.0.0", "@typespec/compiler": "^1.11.0", "@typespec/events": ">=0.81.0 <1.0.0", "@typespec/http": "^1.11.0", @@ -70,12 +70,12 @@ }, "devDependencies": { "@azure-tools/typespec-autorest": "0.67.0", - "@azure-tools/typespec-azure-core": "0.67.0", + "@azure-tools/typespec-azure-core": "0.67.1", "@azure-tools/typespec-azure-resource-manager": "0.67.1", "@azure-tools/typespec-azure-rulesets": "0.67.0", - "@azure-tools/typespec-client-generator-core": "0.67.2", - "@microsoft/api-extractor": "^7.58.2", - "@microsoft/api-extractor-model": "^7.33.6", + "@azure-tools/typespec-client-generator-core": "0.67.3", + "@microsoft/api-extractor": "^7.58.7", + "@microsoft/api-extractor-model": "^7.33.8", "@types/js-yaml": "~4.0.9", "@types/lodash": "~4.17.24", "@types/node": "~25.6.0", @@ -89,11 +89,11 @@ "@typespec/streams": "0.81.0", "@typespec/versioning": "0.81.0", "@typespec/xml": "0.81.0", - "@vitest/coverage-v8": "^4.1.4", - "@vitest/ui": "^4.1.4", + "@vitest/coverage-v8": "^4.1.5", + "@vitest/ui": "^4.1.5", "c8": "~11.0.0", "rimraf": "~6.1.3", - "typescript": "~6.0.2", - "vitest": "^4.1.4" + "typescript": "~6.0.3", + "vitest": "^4.1.5" } } diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index fdcfeeef10c..49110135355 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -42,21 +42,24 @@ function addDefaultOptions(sdkContext: PythonSdkContext) { const packageName = namespace.replace(/\./g, "-"); options["package-name"] = packageName; } - if ((options as any).flavor !== "azure") { - // if they pass in a flavor other than azure, we want to ignore the value - (options as any).flavor = undefined; - } + // Set flavor based on namespace or passed option if (getRootNamespace(sdkContext).toLowerCase().includes("azure")) { (options as any).flavor = "azure"; + } else if ((options as any).flavor !== "azure") { + // Explicitly set unbranded flavor when not azure + (options as any).flavor = "unbranded"; } if ( options["package-pprint-name"] !== undefined && !options["package-pprint-name"].startsWith('"') ) { - options["package-pprint-name"] = options["use-pyodide"] - ? `${options["package-pprint-name"]}` - : `"${options["package-pprint-name"]}"`; + // Only add quotes for shell compatibility when NOT using emit-yaml-only mode + // (emit-yaml-only passes options via JSON config files, not shell) + const needsShellQuoting = !options["use-pyodide"] && !options["emit-yaml-only"]; + options["package-pprint-name"] = needsShellQuoting + ? `"${options["package-pprint-name"]}"` + : `${options["package-pprint-name"]}`; } } @@ -246,6 +249,21 @@ async function onEmitMain(context: EmitContext) { const yamlPath = await saveCodeModelAsYaml("python-yaml-path", parsedYamlMap); if (!program.compilerOptions.noEmit && !program.hasError()) { + // If emit-yaml-only mode, just copy YAML to output dir for batch processing + if (resolvedOptions["emit-yaml-only"]) { + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + // Copy YAML to output dir with command args embedded + // Use unique filename to avoid conflicts when multiple specs share output dir + const configId = path.basename(yamlPath, ".yaml"); + const batchConfig = { yamlPath, commandArgs, outputDir }; + fs.writeFileSync( + path.join(outputDir, `.tsp-codegen-${configId}.json`), + JSON.stringify(batchConfig, null, 2), + ); + return; + } // if not using pyodide and there's no venv, we try to create venv if (!resolvedOptions["use-pyodide"] && !fs.existsSync(path.join(root, "venv"))) { try { diff --git a/packages/http-client-python/emitter/src/lib.ts b/packages/http-client-python/emitter/src/lib.ts index 2d6010108e0..b267c836bf3 100644 --- a/packages/http-client-python/emitter/src/lib.ts +++ b/packages/http-client-python/emitter/src/lib.ts @@ -25,6 +25,7 @@ export interface PythonEmitterOptions { "use-pyodide"?: boolean; "keep-setup-py"?: boolean; "clear-output-folder"?: boolean; + "emit-yaml-only"?: boolean; } export interface PythonSdkContext extends SdkContext { @@ -110,6 +111,12 @@ export const PythonEmitterOptionsSchema: JSONSchemaType = description: "Whether to clear the output folder before generating the code. Defaults to `false`.", }, + "emit-yaml-only": { + type: "boolean", + nullable: true, + description: + "Emit YAML code model only, without running Python generator. For batch processing.", + }, }, required: [], }; diff --git a/packages/http-client-python/emitter/src/types.ts b/packages/http-client-python/emitter/src/types.ts index 3e0ee6f6b32..128549d7821 100644 --- a/packages/http-client-python/emitter/src/types.ts +++ b/packages/http-client-python/emitter/src/types.ts @@ -377,9 +377,6 @@ function emitEnum(context: PythonSdkContext, type: SdkEnumType): Record { pointInTimeUTC: "point_in_time_utc", diskSizeGB: "disk_size_gb", lastModifiedTS: "last_modified_ts", + "FOO-BAR": "foo_bar", + "FOO-BAR-BAZ": "foo_bar_baz", + "A-B": "a_b", }; for (const [input, expected] of Object.entries(cases)) { strictEqual(camelToSnakeCase(input), expected); diff --git a/packages/http-client-python/eng/scripts/Test-Packages.ps1 b/packages/http-client-python/eng/scripts/Test-Packages.ps1 index cbfd684b154..05ea968b8cb 100644 --- a/packages/http-client-python/eng/scripts/Test-Packages.ps1 +++ b/packages/http-client-python/eng/scripts/Test-Packages.ps1 @@ -74,17 +74,6 @@ try { Invoke-LoggedCommand "npm run ci" Write-Host "All tests passed." -ForegroundColor Green - - # Linux specific: check mypy/lint/pyright on generated code - if ($IsLinux) { - Write-Host "`n=== Running lint on generated code ===" -ForegroundColor Cyan - Invoke-LoggedCommand "npm run lint:generated" - Write-Host "Generated code checks passed." -ForegroundColor Green - - Write-Host "`n=== Running mypy/pyright on generated code ===" -ForegroundColor Cyan - Invoke-LoggedCommand "npm run typecheck:generated" - Write-Host "Generated code mypy/pyright checks passed." -ForegroundColor Green - } } } finally { diff --git a/packages/http-client-python/eng/scripts/ci/config/eslint-ci.config.mjs b/packages/http-client-python/eng/scripts/ci/config/eslint-ci.config.mjs index b8a1418a63e..3b51a3fc0d0 100644 --- a/packages/http-client-python/eng/scripts/ci/config/eslint-ci.config.mjs +++ b/packages/http-client-python/eng/scripts/ci/config/eslint-ci.config.mjs @@ -2,7 +2,11 @@ // Standalone eslint config for http-client-python package // This config is used in CI where monorepo dependencies may not be available import eslint from "@eslint/js"; +import { dirname } from "path"; import tsEslint from "typescript-eslint"; +import { fileURLToPath } from "url"; + +const root = dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url))))); export default [ { @@ -11,6 +15,11 @@ export default [ eslint.configs.recommended, ...tsEslint.configs.recommended, { + languageOptions: { + parserOptions: { + tsconfigRootDir: root, + }, + }, rules: { // TypeScript plugin overrides "@typescript-eslint/no-non-null-assertion": "off", @@ -40,6 +49,7 @@ export default [ "no-case-declarations": "off", "no-ex-assign": "off", "no-undef": "off", + "no-useless-assignment": "error", "prefer-const": [ "warn", { diff --git a/packages/http-client-python/eng/scripts/ci/dev_requirements.txt b/packages/http-client-python/eng/scripts/ci/dev_requirements.txt index 2d713225106..5fc4a972cb0 100644 --- a/packages/http-client-python/eng/scripts/ci/dev_requirements.txt +++ b/packages/http-client-python/eng/scripts/ci/dev_requirements.txt @@ -7,6 +7,6 @@ colorama==0.4.6 debugpy==1.8.2 pytest==8.3.2 coverage==7.6.1 -black==24.8.0 +black==26.3.1 ptvsd==4.3.2 types-PyYAML==6.0.12.8 diff --git a/packages/http-client-python/eng/scripts/ci/lint.ts b/packages/http-client-python/eng/scripts/ci/lint.ts index 9d6150b9acc..9dad879a992 100644 --- a/packages/http-client-python/eng/scripts/ci/lint.ts +++ b/packages/http-client-python/eng/scripts/ci/lint.ts @@ -133,14 +133,21 @@ function runCommand( } async function lintEmitter(): Promise { - console.log(`\n${pc.bold("=== Linting TypeScript Emitter ===")}\n`); + console.log(`\n${pc.bold("=== Linting TypeScript ===")}\n`); // Run eslint with local config to avoid dependency on monorepo's eslint.config.js // This ensures the package can be linted in CI without full monorepo dependencies + // Lint both emitter/ and eng/scripts/ directories return runCommand( "eslint", - ["emitter/", "--config", "eng/scripts/ci/config/eslint-ci.config.mjs", "--max-warnings=0"], + [ + "emitter/", + "eng/scripts/", + "--config", + "eng/scripts/ci/config/eslint-ci.config.mjs", + "--max-warnings=0", + ], root, - "eslint emitter/ --max-warnings=0", + "eslint emitter/ eng/scripts/ --max-warnings=0", ); } diff --git a/packages/http-client-python/eng/scripts/ci/regenerate.ts b/packages/http-client-python/eng/scripts/ci/regenerate.ts index d49f5ac4d71..958c871849e 100644 --- a/packages/http-client-python/eng/scripts/ci/regenerate.ts +++ b/packages/http-client-python/eng/scripts/ci/regenerate.ts @@ -7,7 +7,8 @@ */ import { compile, NodeHost } from "@typespec/compiler"; -import { rmSync } from "fs"; +import { execSync } from "child_process"; +import { existsSync, readdirSync, rmSync } from "fs"; import { platform } from "os"; import { dirname, join, relative, resolve } from "path"; import pc from "picocolors"; @@ -173,6 +174,9 @@ function buildTaskGroups(specs: string[], flags: RegenerateFlags): TaskGroup[] { // Examples directory options["examples-dir"] = toPosix(join(dirname(spec), "examples")); + // Emit YAML only - Python processing is batched after all specs compile + options["emit-yaml-only"] = true; + tasks.push({ spec, outputDir, options }); } @@ -213,6 +217,25 @@ async function compileSpec(task: CompileTask): Promise<{ success: boolean; error } } +function renderProgressBar( + completed: number, + failed: number, + total: number, + width: number = 40, +): string { + const successCount = completed - failed; + const successWidth = Math.round((successCount / total) * width); + const failWidth = Math.round((failed / total) * width); + const emptyWidth = width - successWidth - failWidth; + + const successBar = pc.bgGreen(" ".repeat(successWidth)); + const failBar = failed > 0 ? pc.bgRed(" ".repeat(failWidth)) : ""; + const emptyBar = pc.dim("░".repeat(Math.max(0, emptyWidth))); + + const percent = Math.round((completed / total) * 100); + return `${successBar}${failBar}${emptyBar} ${pc.cyan(`${percent}%`)} (${completed}/${total})`; +} + async function runParallel(groups: TaskGroup[], maxJobs: number): Promise> { const results = new Map(); const executing: Set> = new Set(); @@ -220,6 +243,20 @@ async function runParallel(groups: TaskGroup[], maxJobs: number): Promise sum + g.tasks.length, 0); let completed = 0; + let failed = 0; + const failedSpecs: string[] = []; + + // Check if we're in a TTY for progress bar updates + const isTTY = process.stdout.isTTY; + + const updateProgress = () => { + if (isTTY) { + process.stdout.write(`\r${renderProgressBar(completed, failed, totalTasks)}`); + } + }; + + // Initial progress bar + updateProgress(); for (const group of groups) { // Each group runs as a unit - tasks within a group run sequentially @@ -232,19 +269,17 @@ async function runParallel(groups: TaskGroup[], maxJobs: number): Promise 0) { + console.log(pc.red(`\nFailed specs:`)); + for (const spec of failedSpecs) { + console.log(pc.red(` • ${spec}`)); + } + } + return results; } +function collectConfigFiles(generatedDir: string, flavor: string): string[] { + const flavorDir = join(generatedDir, "..", "tests", "generated", flavor); + if (!existsSync(flavorDir)) return []; + + const configFiles: string[] = []; + for (const pkg of readdirSync(flavorDir, { withFileTypes: true })) { + if (pkg.isDirectory()) { + const pkgDir = join(flavorDir, pkg.name); + // Find all .tsp-codegen-*.json files (supports multiple configs per output dir) + for (const file of readdirSync(pkgDir)) { + if (file.startsWith(".tsp-codegen-") && file.endsWith(".json")) { + configFiles.push(join(pkgDir, file)); + } + } + } + } + return configFiles; +} + +function runBatchPythonProcessing(flavor: string, configCount: number, jobs: number): boolean { + if (configCount === 0) return true; + + console.log(pc.cyan(`\nRunning batch Python processing on ${configCount} specs...`)); + + // Find Python venv + let venvPath = join(PLUGIN_DIR, "venv"); + if (existsSync(join(venvPath, "bin"))) { + venvPath = join(venvPath, "bin", "python"); + } else if (existsSync(join(venvPath, "Scripts"))) { + venvPath = join(venvPath, "Scripts", "python.exe"); + } else { + console.error(pc.red("Python venv not found")); + return false; + } + + const batchScript = join(PLUGIN_DIR, "eng", "scripts", "setup", "run_batch.py"); + + try { + // Pass directory and flavor instead of individual config files to avoid command line length limits on Windows + execSync( + `"${venvPath}" "${batchScript}" --generated-dir "${PLUGIN_DIR}" --flavor ${flavor} --jobs ${jobs}`, + { + stdio: "inherit", + cwd: PLUGIN_DIR, + }, + ); + return true; + } catch { + return false; + } +} + async function regenerateFlavor( flavor: string, name: string | undefined, @@ -289,21 +390,46 @@ async function regenerateFlavor( console.log(pc.cyan(`Found ${allSpecs.length} specs (${totalTasks} total tasks) to compile`)); console.log(pc.cyan(`Using ${jobs} parallel jobs\n`)); - // Run compilation + // Run compilation (emits YAML only) const startTime = performance.now(); const results = await runParallel(groups, jobs); - const duration = (performance.now() - startTime) / 1000; + const compileTime = (performance.now() - startTime) / 1000; - // Summary + // Summary for TypeSpec compilation const succeeded = Array.from(results.values()).filter((v) => v).length; - const failed = results.size - succeeded; + const compileFailed = results.size - succeeded; + + console.log( + pc.cyan( + `\nTypeSpec compilation: ${succeeded} succeeded, ${compileFailed} failed (${compileTime.toFixed(1)}s)`, + ), + ); + + if (compileFailed > 0) { + console.log(pc.red(`Skipping Python processing due to compilation failures`)); + return false; + } + + // Batch process all specs with Python + const pyStartTime = performance.now(); + const configCount = collectConfigFiles(GENERATED_FOLDER, flavor).length; + // Use fewer Python jobs since Python processing is heavier + const pyJobs = Math.max(4, jobs); + const pySuccess = runBatchPythonProcessing(flavor, configCount, pyJobs); + const pyTime = (performance.now() - pyStartTime) / 1000; + + const totalTime = (performance.now() - startTime) / 1000; console.log(pc.cyan(`\n${"=".repeat(60)}`)); - console.log(pc.cyan(`Results: ${succeeded} succeeded, ${failed} failed`)); - console.log(pc.cyan(`Time: ${duration.toFixed(1)}s`)); + console.log(pc.cyan(`Results: ${succeeded} specs processed`)); + console.log( + pc.cyan( + ` TypeSpec: ${compileTime.toFixed(1)}s | Python: ${pyTime.toFixed(1)}s | Total: ${totalTime.toFixed(1)}s`, + ), + ); console.log(pc.cyan(`${"=".repeat(60)}\n`)); - return failed === 0; + return pySuccess; } async function main() { diff --git a/packages/http-client-python/eng/scripts/ci/run-tests.ts b/packages/http-client-python/eng/scripts/ci/run-tests.ts index c5409113dff..969ff746346 100644 --- a/packages/http-client-python/eng/scripts/ci/run-tests.ts +++ b/packages/http-client-python/eng/scripts/ci/run-tests.ts @@ -35,7 +35,7 @@ Options: -f, --flavor SDK flavor to test (only applies to --generator) If not specified, tests both flavors --env Specific tox environments to run - Available: test, lint, mypy, pyright, docs, ci, unittest + Available: test, lint, mypy, pyright, apiview, sphinx, docs, ci, unittest -j, --jobs Number of parallel jobs (default: CPU cores - 2) -n, --name Filter tests by name pattern -q, --quiet Suppress test output (only show pass/fail summary) @@ -46,8 +46,10 @@ Environments (for --generator): lint Run pylint on generated packages mypy Run mypy type checking on generated packages pyright Run pyright type checking on generated packages - docs Run documentation validation (apiview, sphinx) - ci Run all checks (test + lint + mypy + pyright) + apiview Run apiview validation on generated packages + sphinx Run sphinx docstring validation on generated packages + docs Run apiview + sphinx (split into parallel envs) + ci Run all checks (test + lint + mypy + pyright + apiview + sphinx) unittest Run unit tests for pygen internals Examples: @@ -56,8 +58,8 @@ Examples: run-tests.ts --generator # Run generator tests for all flavors run-tests.ts --generator --flavor=azure # Run generator tests for azure only run-tests.ts -g -f azure --env=test # Run pytest for azure only - run-tests.ts -g --env=mypy # Run mypy for all flavors - run-tests.ts -g -f unbranded --env=lint # Run pylint for unbranded only + run-tests.ts -g --env=lint # Run pylint for all flavors + run-tests.ts -g -f unbranded --env=mypy # Run mypy for unbranded only `); process.exit(0); } @@ -80,7 +82,13 @@ interface ToxResult { error?: string; } -async function runToxEnv(env: string, pythonPath: string, name?: string): Promise { +interface RunningTask { + env: string; + proc: ChildProcess; + promise: Promise; +} + +function startToxEnv(env: string, pythonPath: string, name?: string): RunningTask { const startTime = Date.now(); const toxIniPath = join(testsDir, "tox.ini"); @@ -92,20 +100,20 @@ async function runToxEnv(env: string, pythonPath: string, name?: string): Promis console.log(`${pc.blue("[START]")} ${env}`); - return new Promise((resolve) => { - const proc: ChildProcess = spawn(pythonPath, args, { - cwd: testsDir, - stdio: !argv.values.quiet ? "inherit" : "pipe", - env: { ...process.env, FOLDER: env.split("-")[1] || "azure" }, - }); + const proc: ChildProcess = spawn(pythonPath, args, { + cwd: testsDir, + stdio: !argv.values.quiet ? "inherit" : "pipe", + env: { ...process.env, FOLDER: env.split("-")[1] || "azure" }, + }); - let stderr = ""; - if (argv.values.quiet && proc.stderr) { - proc.stderr.on("data", (data) => { - stderr += data.toString(); - }); - } + let stderr = ""; + if (argv.values.quiet && proc.stderr) { + proc.stderr.on("data", (data) => { + stderr += data.toString(); + }); + } + const promise = new Promise((resolve) => { proc.on("close", (code) => { const duration = (Date.now() - startTime) / 1000; const success = code === 0; @@ -135,6 +143,16 @@ async function runToxEnv(env: string, pythonPath: string, name?: string): Promis }); }); }); + + return { env, proc, promise }; +} + +function killTask(task: RunningTask): void { + try { + task.proc.kill("SIGTERM"); + } catch { + // Process may have already exited + } } async function runParallel( @@ -144,24 +162,51 @@ async function runParallel( name?: string, ): Promise { const results: ToxResult[] = []; - const running: Map> = new Map(); + const running: Map = new Map(); for (const env of envs) { // Wait if we're at max capacity - if (running.size >= maxJobs) { - const completed = await Promise.race(running.values()); + while (running.size >= maxJobs) { + const promises = Array.from(running.values()).map((t) => t.promise); + const completed = await Promise.race(promises); results.push(completed); running.delete(completed.env); + + // Fail-fast: kill all running tasks and exit + if (!completed.success) { + console.log(pc.red(`\n[FAIL-FAST] ${completed.env} failed, killing remaining tasks...`)); + for (const task of running.values()) { + killTask(task); + } + // Wait briefly for processes to terminate + await Promise.all(Array.from(running.values()).map((t) => t.promise)); + return results; + } } // Start new task - const task = runToxEnv(env, pythonPath, name); + const task = startToxEnv(env, pythonPath, name); running.set(env, task); } - // Wait for remaining tasks - const remaining = await Promise.all(running.values()); - results.push(...remaining); + // Wait for remaining tasks, checking for failures + while (running.size > 0) { + const promises = Array.from(running.values()).map((t) => t.promise); + const completed = await Promise.race(promises); + results.push(completed); + running.delete(completed.env); + + // Fail-fast: kill all running tasks and exit + if (!completed.success) { + console.log(pc.red(`\n[FAIL-FAST] ${completed.env} failed, killing remaining tasks...`)); + for (const task of running.values()) { + killTask(task); + } + // Wait briefly for processes to terminate + await Promise.all(Array.from(running.values()).map((t) => t.promise)); + return results; + } + } return results; } @@ -316,12 +361,15 @@ async function main(): Promise { baseEnvs = ["test"]; } - // Expand 'ci' into its component environments for parallel execution + // Expand 'ci' and 'docs' into component environments for parallel execution const expandedEnvs: string[] = []; for (const env of baseEnvs) { if (env === "ci") { - // Run test first (sequential), then lint/mypy/pyright/docs in parallel - expandedEnvs.push("test", "lint", "mypy", "pyright", "docs"); + // All envs run in parallel — pre-built wheels make installs cheap + expandedEnvs.push("test", "lint", "mypy", "pyright", "apiview", "sphinx"); + } else if (env === "docs") { + // Split docs into apiview + sphinx for parallelism + expandedEnvs.push("apiview", "sphinx"); } else { expandedEnvs.push(env); } @@ -348,37 +396,48 @@ async function main(): Promise { process.exit(1); } - // Separate test environments from other environments - // Test environments must run sequentially (they share port 3000) - // Other environments (lint, mypy, pyright, docs) can run in parallel - const testEnvs = envs.filter((e) => e.startsWith("test-") || e === "unittest"); - const otherEnvs = envs.filter((e) => !e.startsWith("test-") && e !== "unittest"); - const maxJobs = argv.values.jobs ? parseInt(argv.values.jobs, 10) : Math.max(2, cpus().length - 2); console.log(` Flavors: ${flavors.join(", ")}`); console.log(` Environments: ${envs.join(", ")}`); - console.log(` Jobs: ${maxJobs} (test envs run sequentially, others in parallel)`); + console.log(` Jobs: ${maxJobs}`); if (argv.values.name) { console.log(` Filter: ${argv.values.name}`); } console.log(); - // Run test environments first (sequentially) - let results: ToxResult[] = []; - if (testEnvs.length > 0) { - console.log(pc.cyan("Running test environments (sequential)...")); - results = await runParallel(testEnvs, pythonPath, 1, argv.values.name); + // Pre-build wheels for each flavor so tox envs install from pre-built + // wheels instead of rebuilding from source (~2min build once vs ~2min × N envs) + console.log(pc.cyan("Pre-building wheels for all flavors...")); + const installScript = join(testsDir, "install_packages.py"); + for (const flavor of flavors) { + const startTime = Date.now(); + const proc = spawn(pythonPath, [installScript, "build", flavor, testsDir], { + cwd: testsDir, + stdio: "inherit", + }); + await new Promise((resolve) => { + proc.on("close", (code) => { + const duration = ((Date.now() - startTime) / 1000).toFixed(1); + if (code === 0) { + console.log(`${pc.green("[PASS]")} wheel build ${flavor} (${duration}s)`); + } else { + console.log( + `${pc.yellow("[WARN]")} wheel build ${flavor} failed (${duration}s), tox envs will build from source`, + ); + } + resolve(); + }); + }); } + console.log(); - // Run other environments in parallel - if (otherEnvs.length > 0) { - console.log(pc.cyan("\nRunning lint/typecheck environments (parallel)...")); - const otherResults = await runParallel(otherEnvs, pythonPath, maxJobs, argv.values.name); - results = results.concat(otherResults); - } + // Run all environments in parallel + // The mock server serves both azure and unbranded specs, so all tests can run together + console.log(pc.cyan("Running all environments in parallel...")); + const results = await runParallel(envs, pythonPath, maxJobs, argv.values.name); allResults.push(...results); } diff --git a/packages/http-client-python/eng/scripts/ci/run_apiview.py b/packages/http-client-python/eng/scripts/ci/run_apiview.py index 5345f694ef0..48d6f890a34 100644 --- a/packages/http-client-python/eng/scripts/ci/run_apiview.py +++ b/packages/http-client-python/eng/scripts/ci/run_apiview.py @@ -10,32 +10,38 @@ import os import sys -from subprocess import check_call, CalledProcessError +from subprocess import run, TimeoutExpired import logging from util import run_check logging.getLogger().setLevel(logging.INFO) +# Timeout for each apiview generation (seconds) +APIVIEW_TIMEOUT = 30 + def _single_dir_apiview(mod): - loop = 0 - while True: + for attempt in range(2): try: - check_call( - [ - "apistubgen", - "--pkg-path", - str(mod.absolute()), - ] + result = run( + ["apistubgen", "--pkg-path", str(mod.absolute())], + capture_output=True, + timeout=APIVIEW_TIMEOUT, ) - except CalledProcessError as e: - if loop >= 2: # retry for maximum 3 times because sometimes the apistubgen has transient failure. - logging.error("{} exited with apiview generation error {}".format(mod.stem, e.returncode)) + if result.returncode == 0: + return True + if attempt == 1: + logging.error(f"{mod.stem} failed: {result.stderr.decode()[:200]}") + return False + except TimeoutExpired: + if attempt == 1: + logging.error(f"{mod.stem} timed out after {APIVIEW_TIMEOUT}s") + return False + except Exception as e: + if attempt == 1: + logging.error(f"{mod.stem} error: {e}") return False - else: - loop += 1 - continue - return True + return False if __name__ == "__main__": diff --git a/packages/http-client-python/eng/scripts/ci/run_mypy.py b/packages/http-client-python/eng/scripts/ci/run_mypy.py index 32443974ead..fb6210f72a2 100644 --- a/packages/http-client-python/eng/scripts/ci/run_mypy.py +++ b/packages/http-client-python/eng/scripts/ci/run_mypy.py @@ -12,7 +12,7 @@ import os import logging import sys -from util import run_check +from util import run_check, get_package_namespace_dir logging.getLogger().setLevel(logging.INFO) @@ -27,9 +27,10 @@ def get_config_file_location(): def _single_dir_mypy(mod): - # Exclude "build" directories to avoid mypy "Duplicate module" errors caused by - # stale build/lib/ artifacts from previous setup.py builds. - inner_class = next(d for d in mod.iterdir() if d.is_dir() and not str(d).endswith("egg-info") and d.stem != "build") + inner_class = get_package_namespace_dir(mod) + if not inner_class: + logging.info(f"No package directory found in {mod}, skipping") + return True try: check_call( [ diff --git a/packages/http-client-python/eng/scripts/ci/run_pylint.py b/packages/http-client-python/eng/scripts/ci/run_pylint.py index f090b8c913a..5a44df63e9c 100644 --- a/packages/http-client-python/eng/scripts/ci/run_pylint.py +++ b/packages/http-client-python/eng/scripts/ci/run_pylint.py @@ -12,7 +12,7 @@ import os import logging import sys -from util import run_check +from util import run_check, get_package_namespace_dir logging.getLogger().setLevel(logging.INFO) @@ -27,11 +27,10 @@ def get_rfc_file_location(): def _single_dir_pylint(mod): - # Exclude "build" directories created by pip install / setup.py build. - # Without this, "build" may be picked first alphabetically and pylint would - # lint stale build artifacts instead of the actual source, causing false - # positives (e.g. modules named "json", "xml", "datetime" shadow the stdlib). - inner_class = next(d for d in mod.iterdir() if d.is_dir() and not str(d).endswith("egg-info") and d.name != "build") + inner_class = get_package_namespace_dir(mod) + if not inner_class: + logging.info(f"No package directory found in {mod}, skipping") + return True # Only load the Azure pylint guidelines checker plugin for azure packages. # The plugin (azure-pylint-guidelines-checker) is only installed in the # lint-azure tox environment and is not available for unbranded packages. diff --git a/packages/http-client-python/eng/scripts/ci/run_pyright.py b/packages/http-client-python/eng/scripts/ci/run_pyright.py index a86ccc2b3cd..b998b515d22 100644 --- a/packages/http-client-python/eng/scripts/ci/run_pyright.py +++ b/packages/http-client-python/eng/scripts/ci/run_pyright.py @@ -13,7 +13,7 @@ import logging import sys import time -from util import run_check +from util import run_check, get_package_namespace_dir logging.getLogger().setLevel(logging.INFO) @@ -28,7 +28,10 @@ def get_pyright_config_file_location(): def _single_dir_pyright(mod): - inner_class = next(d for d in mod.iterdir() if d.is_dir() and not str(d).endswith("egg-info")) + inner_class = get_package_namespace_dir(mod) + if not inner_class: + logging.info(f"No package directory found in {mod}, skipping") + return True retries = 3 while retries: try: diff --git a/packages/http-client-python/eng/scripts/ci/run_sphinx_build.py b/packages/http-client-python/eng/scripts/ci/run_sphinx_build.py index e97476aed9e..0dba25b8f68 100644 --- a/packages/http-client-python/eng/scripts/ci/run_sphinx_build.py +++ b/packages/http-client-python/eng/scripts/ci/run_sphinx_build.py @@ -8,18 +8,21 @@ # This script is used to execute sphinx documentation build within a tox environment. # It uses a central sphinx configuration and validates docstrings by running sphinx-build. -from subprocess import check_call, CalledProcessError +from subprocess import run, TimeoutExpired import os import logging import sys from pathlib import Path -from util import run_check +from util import run_check, SKIP_PACKAGE_DIRS logging.getLogger().setLevel(logging.INFO) # Get the central Sphinx config directory SPHINX_CONF_DIR = os.path.abspath(os.path.dirname(__file__)) +# Timeout for each sphinx build (seconds) +SPHINX_TIMEOUT = 120 + def _create_minimal_index_rst(docs_dir, package_name, module_names): """Create a minimal index.rst file for sphinx to process.""" @@ -50,7 +53,12 @@ def _single_dir_sphinx(mod): # Find the actual Python package directories package_dirs = [ - d for d in mod.iterdir() if d.is_dir() and not d.name.startswith("_") and (d / "__init__.py").exists() + d + for d in mod.iterdir() + if d.is_dir() + and not d.name.startswith("_") + and d.name not in SKIP_PACKAGE_DIRS + and (d / "__init__.py").exists() ] if not package_dirs: @@ -85,7 +93,7 @@ def _single_dir_sphinx(mod): sys.path.insert(0, str(mod.absolute())) try: - result = check_call( + result = run( [ sys.executable, "-m", @@ -100,12 +108,19 @@ def _single_dir_sphinx(mod): "-q", # Quiet mode (only show warnings/errors) str(docs_dir.absolute()), # Source directory str(output_dir.absolute()), # Output directory - ] + ], + capture_output=True, + timeout=SPHINX_TIMEOUT, ) - logging.info(f"Sphinx build completed successfully for {mod.stem}") - return True - except CalledProcessError as e: - logging.error(f"{mod.stem} exited with sphinx build error {e.returncode}") + if result.returncode == 0: + return True + logging.error(f"{mod.stem} sphinx error: {result.stderr.decode()[:500]}") + return False + except TimeoutExpired: + logging.error(f"{mod.stem} timed out after {SPHINX_TIMEOUT}s") + return False + except Exception as e: + logging.error(f"{mod.stem} sphinx error: {e}") return False finally: # Remove from sys.path diff --git a/packages/http-client-python/eng/scripts/ci/util.py b/packages/http-client-python/eng/scripts/ci/util.py index 63a51c4d754..07aba0f3f49 100644 --- a/packages/http-client-python/eng/scripts/ci/util.py +++ b/packages/http-client-python/eng/scripts/ci/util.py @@ -8,14 +8,32 @@ import logging from pathlib import Path import argparse -from multiprocessing import Pool +from concurrent.futures import ProcessPoolExecutor, as_completed logging.getLogger().setLevel(logging.INFO) +# Root is the tests directory (4 levels up from this file: ci -> scripts -> eng -> package_root, then into tests) ROOT_FOLDER = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "tests")) IGNORE_FOLDER = [] +# Directories inside each generated package that should be skipped by all CI checks. +# These are auto-generated test/sample scaffolding, not the actual SDK code. +SKIP_PACKAGE_DIRS = {"generated_tests", "generated_samples", "build", "__pycache__", ".pytest_cache"} + + +def get_package_namespace_dir(mod): + """Find the actual namespace directory inside a generated package, skipping non-SDK dirs.""" + for d in mod.iterdir(): + if ( + d.is_dir() + and not d.name.startswith("_") + and not d.name.endswith("egg-info") + and d.name not in SKIP_PACKAGE_DIRS + ): + return d + return None + def run_check(name, call_back, log_info): parser = argparse.ArgumentParser( @@ -25,16 +43,9 @@ def run_check(name, call_back, log_info): "-t", "--test-folder", dest="test_folder", - help="The test folder we're in. Can be 'azure' or 'vanilla'", + help="The test folder we're in. Can be 'azure' or 'unbranded'", required=True, ) - parser.add_argument( - "-g", - "--generator", - dest="generator", - help="The generator we're using. Can be 'legacy', 'version-tolerant'.", - required=False, - ) parser.add_argument( "-f", "--file-name", @@ -46,28 +57,52 @@ def run_check(name, call_back, log_info): "-s", "--subfolder", dest="subfolder", - help="The specific sub folder to validate, default to Expected/AcceptanceTests. Optional.", + help="The subfolder containing generated code, default to 'generated'.", required=False, - default="Expected/AcceptanceTests", + default="generated", + ) + parser.add_argument( + "-j", + "--jobs", + dest="jobs", + help="Number of parallel jobs (default: CPU count)", + type=int, + required=False, + default=max(1, os.cpu_count()), ) args = parser.parse_args() - pkg_dir = Path(ROOT_FOLDER) - if args.subfolder: - pkg_dir /= Path(args.subfolder) - pkg_dir /= Path(args.test_folder) - if args.generator: - pkg_dir /= Path(args.generator) + # Path structure: tests/generated/{test_folder}/ + pkg_dir = Path(ROOT_FOLDER) / Path(args.subfolder) / Path(args.test_folder) dirs = [d for d in pkg_dir.iterdir() if d.is_dir() and not d.stem.startswith("_") and d.stem not in IGNORE_FOLDER] if args.file_name: dirs = [d for d in dirs if args.file_name.lower() in d.stem.lower()] - if len(dirs) > 1: - with Pool() as pool: - result = pool.map(call_back, dirs) - response = all(result) - else: - response = call_back(dirs[0]) - if not response: - logging.error("%s fails", log_info) + + if not dirs: + logging.info("No directories to process") + return + + logging.info(f"Processing {len(dirs)} packages with {args.jobs} parallel jobs...") + + failed = [] + succeeded = 0 + + with ProcessPoolExecutor(max_workers=args.jobs) as executor: + futures = {executor.submit(call_back, d): d for d in dirs} + for future in as_completed(futures): + pkg = futures[future] + try: + if future.result(): + succeeded += 1 + else: + failed.append(pkg.stem) + except Exception as e: + logging.error(f"{pkg.stem} raised exception: {e}") + failed.append(pkg.stem) + + logging.info(f"{log_info}: {succeeded} succeeded, {len(failed)} failed") + + if failed: + logging.error(f"{log_info} failed for: {', '.join(failed)}") exit(1) diff --git a/packages/http-client-python/eng/scripts/setup/run_batch.py b/packages/http-client-python/eng/scripts/setup/run_batch.py new file mode 100644 index 00000000000..9548962b417 --- /dev/null +++ b/packages/http-client-python/eng/scripts/setup/run_batch.py @@ -0,0 +1,186 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +""" +Batch process multiple TypeSpec YAML files in a single Python process. +This avoids the overhead of spawning a new Python process for each spec. +""" + +import argparse +import json +import sys +import os +from pathlib import Path +from concurrent.futures import ProcessPoolExecutor, as_completed +from multiprocessing import freeze_support + +# Add the generator to the path +_ROOT_DIR = Path(__file__).parent.parent.parent.parent +sys.path.insert(0, str(_ROOT_DIR / "generator")) + + +def process_single_spec(config_path_str: str) -> tuple[str, bool, str]: + """Process a single spec from its config file. + + Returns: (output_dir, success, error_message) + """ + # Import inside function for multiprocessing compatibility + from pygen import preprocess, codegen + + config_path = Path(config_path_str) + try: + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + + yaml_path = config["yamlPath"] + command_args = config["commandArgs"] + output_dir = config["outputDir"] + + # Pass command args directly to pygen - pygen expects hyphenated keys + # Remove keys that shouldn't be passed to pygen + pygen_args = {k: v for k, v in command_args.items() if k not in ["emit-yaml-only"]} + + # Run preprocess and codegen (black is batched at the end for performance) + preprocess.PreProcessPlugin(output_folder=output_dir, tsp_file=yaml_path, **pygen_args).process() + + codegen.CodeGenerator(output_folder=output_dir, tsp_file=yaml_path, **pygen_args).process() + + # Clean up the config file + config_path.unlink() + + return (output_dir, True, "") + except Exception as e: + return (str(config_path), False, str(e)) + + +def render_progress_bar(completed: int, failed: int, total: int, width: int = 40) -> str: + """Render a progress bar with green for success and red for failures.""" + success_count = completed - failed + success_width = round((success_count / total) * width) if total > 0 else 0 + fail_width = round((failed / total) * width) if total > 0 else 0 + empty_width = width - success_width - fail_width + + # ANSI color codes + green_bg = "\033[42m" + red_bg = "\033[41m" + reset = "\033[0m" + dim = "\033[2m" + cyan = "\033[36m" + + success_bar = f"{green_bg}{' ' * success_width}{reset}" + fail_bar = f"{red_bg}{' ' * fail_width}{reset}" if failed > 0 else "" + empty_bar = f"{dim}{'░' * max(0, empty_width)}{reset}" + + percent = round((completed / total) * 100) if total > 0 else 0 + return f"{success_bar}{fail_bar}{empty_bar} {cyan}{percent}%{reset} ({completed}/{total})" + + +def collect_config_files(generated_dir: str, flavor: str) -> list[str]: + """Collect all .tsp-codegen-*.json config files from the generated directory.""" + flavor_dir = Path(generated_dir) / "tests" / "generated" / flavor + if not flavor_dir.exists(): + return [] + + config_files = [] + for pkg_dir in flavor_dir.iterdir(): + if pkg_dir.is_dir(): + for f in pkg_dir.iterdir(): + if f.name.startswith(".tsp-codegen-") and f.name.endswith(".json"): + config_files.append(str(f)) + return config_files + + +def main(): + parser = argparse.ArgumentParser(description="Batch process TypeSpec YAML files") + parser.add_argument( + "--generated-dir", + required=True, + help="Path to the generator directory (config files are in ../tests/generated//)", + ) + parser.add_argument( + "--flavor", + required=True, + help="Flavor to process (azure or unbranded)", + ) + parser.add_argument( + "--jobs", + type=int, + default=4, + help="Number of parallel jobs (default: 4)", + ) + args = parser.parse_args() + + # Discover config files from the generated directory + config_files = collect_config_files(args.generated_dir, args.flavor) + total = len(config_files) + + if total == 0: + print("No config files found, nothing to process") + return + + print(f"Processing {total} specs with {args.jobs} parallel jobs...") + + succeeded = 0 + failed = 0 + failed_specs = [] + output_dirs = [] + is_tty = sys.stdout.isatty() + + def update_progress(): + if is_tty: + sys.stdout.write(f"\r{render_progress_bar(succeeded + failed, failed, total)}") + sys.stdout.flush() + + # Initial progress bar + update_progress() + + # Use ProcessPoolExecutor for true parallelism (bypasses GIL) + with ProcessPoolExecutor(max_workers=args.jobs) as executor: + futures = {executor.submit(process_single_spec, cf): cf for cf in config_files} + + for future in as_completed(futures): + output_dir, success, error = future.result() + if success: + succeeded += 1 + output_dirs.append(output_dir) + else: + failed += 1 + failed_specs.append(f"{output_dir}: {error}") + # Fail-fast: cancel pending futures on first failure + print(f"\n\033[31m[FAIL-FAST] Cancelling remaining tasks after failure...\033[0m") + for f in futures: + f.cancel() + break + update_progress() + + # Clear progress bar line + if is_tty: + sys.stdout.write("\r" + " " * 60 + "\r") + sys.stdout.flush() + + # Print failures at the end + if failed_specs: + print("\n\033[31mFailed specs:\033[0m") + for spec in failed_specs: + print(f" \033[31m•\033[0m {spec}") + + print(f"\nBatch processing complete: {succeeded} succeeded, {failed} failed") + + if failed > 0: + sys.exit(1) + + # Run black formatting after all codegen completes. Running black separately + # avoids duplicating black's import/startup cost in each worker process. + if output_dirs: + from pygen.black import BlackScriptPlugin + + print(f"Formatting {len(output_dirs)} packages with black...") + for d in output_dirs: + BlackScriptPlugin(output_folder=d).process() + + +if __name__ == "__main__": + freeze_support() # Required for Windows multiprocessing + main() diff --git a/packages/http-client-python/eng/scripts/setup/venvtools.py b/packages/http-client-python/eng/scripts/setup/venvtools.py index c4afe0bcf84..15a89fbcaa6 100644 --- a/packages/http-client-python/eng/scripts/setup/venvtools.py +++ b/packages/http-client-python/eng/scripts/setup/venvtools.py @@ -8,7 +8,6 @@ import sys from pathlib import Path - # eng/scripts/setup/venvtools.py -> need to go up 4 levels to get to package root _ROOT_DIR = Path(__file__).parent.parent.parent.parent diff --git a/packages/http-client-python/generator/pygen/__init__.py b/packages/http-client-python/generator/pygen/__init__.py index ceb67faa88e..91cfab71f69 100644 --- a/packages/http-client-python/generator/pygen/__init__.py +++ b/packages/http-client-python/generator/pygen/__init__.py @@ -16,7 +16,6 @@ from ._version import VERSION - __version__ = VERSION _LOGGER = logging.getLogger(__name__) diff --git a/packages/http-client-python/generator/pygen/codegen/__init__.py b/packages/http-client-python/generator/pygen/codegen/__init__.py index fc8931c83a4..74e3031729c 100644 --- a/packages/http-client-python/generator/pygen/codegen/__init__.py +++ b/packages/http-client-python/generator/pygen/codegen/__init__.py @@ -13,7 +13,6 @@ from .models.code_model import CodeModel from .serializers import JinjaSerializer - _LOGGER = logging.getLogger(__name__) diff --git a/packages/http-client-python/generator/pygen/codegen/models/base.py b/packages/http-client-python/generator/pygen/codegen/models/base.py index a27a65b0a72..0921790670d 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/base.py +++ b/packages/http-client-python/generator/pygen/codegen/models/base.py @@ -7,7 +7,6 @@ from abc import ABC, abstractmethod from .imports import FileImport - if TYPE_CHECKING: from .code_model import CodeModel from .model_type import ModelType diff --git a/packages/http-client-python/generator/pygen/codegen/models/enum_type.py b/packages/http-client-python/generator/pygen/codegen/models/enum_type.py index 8a4171a0337..9cbec3d1b30 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/enum_type.py +++ b/packages/http-client-python/generator/pygen/codegen/models/enum_type.py @@ -10,7 +10,6 @@ from .utils import NamespaceType, add_to_pylint_disable from ...utils import NAME_LENGTH_LIMIT - if TYPE_CHECKING: from .code_model import CodeModel diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py b/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py index 018605575c9..a95b1fd2f27 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py @@ -180,6 +180,15 @@ def serialize(self) -> None: elif client_namespace_type.clients: # add clients folder if there are clients in this namespace self._serialize_client_and_config_files(client_namespace, client_namespace_type.clients, env) + # When generation-subdir is configured, generated code goes into a subdirectory + # (e.g., _generated/). We also need an __init__.py in the parent namespace dir + # so that the package is discoverable by find_packages() / pip install. + if self.code_model.options.get("generation-subdir"): + root_dir = self.code_model.get_root_dir() + self.write_file( + root_dir / Path("__init__.py"), + general_serializer.serialize_pkgutil_init_file(), + ) else: # add pkgutil init file if no clients in this namespace self.write_file( @@ -242,7 +251,7 @@ def _serialize_and_write_package_files(self) -> None: lstrip_blocks=True, ) - package_files = _PACKAGE_FILES + package_files = list(_PACKAGE_FILES) # Copy to avoid modifying global if not self.code_model.license_description: package_files.remove("LICENSE.jinja2") elif Path(self.code_model.options["package-mode"]).exists(): diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py index 54fe489920d..ce907c55427 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py @@ -155,7 +155,10 @@ def serialize_package_file(self, template_name: str, file_content: str, **kwargs "VERSION_MAP": VERSION_MAP, "MIN_PYTHON_VERSION": MIN_PYTHON_VERSION, "MAX_PYTHON_VERSION": MAX_PYTHON_VERSION, - "ADDITIONAL_DEPENDENCIES": [f"{item[0]}>={item[1]}" for item in additional_version_map.items()], + "ADDITIONAL_DEPENDENCIES": [ + dep if dep.startswith('"') else f'"{dep}"' + for dep in (f"{item[0]}>={item[1]}" for item in additional_version_map.items()) + ], } params |= {"options": self.code_model.options} params |= kwargs diff --git a/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 index 5cc41d91cb9..4a012849501 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 @@ -57,7 +57,7 @@ dependencies = [ {% endfor %} {% endif %} {% for dep in ADDITIONAL_DEPENDENCIES %} - "{{ dep }}", + {{ dep }}, {% endfor %} ] dynamic = [ diff --git a/packages/http-client-python/generator/setup.py b/packages/http-client-python/generator/setup.py index ad746f103a1..99996b606f3 100644 --- a/packages/http-client-python/generator/setup.py +++ b/packages/http-client-python/generator/setup.py @@ -47,7 +47,7 @@ ] ), install_requires=[ - "black==24.8.0", + "black==26.3.1", "docutils>=0.20.1", "Jinja2==3.1.6", "PyYAML==6.0.1", diff --git a/packages/http-client-python/package.json b/packages/http-client-python/package.json index 1b911555093..644950c2daf 100644 --- a/packages/http-client-python/package.json +++ b/packages/http-client-python/package.json @@ -46,7 +46,7 @@ "typecheck": "tsx ./eng/scripts/ci/typecheck.ts", "typecheck:generated": "tsx ./eng/scripts/ci/typecheck.ts --generated", "regenerate": "tsx ./eng/scripts/ci/regenerate.ts", - "ci": "npm run test && npm run lint && npm run typecheck", + "ci": "npm run test:emitter && npm run ci:generated", "ci:generated": "tsx ./eng/scripts/ci/run-tests.ts --generator --env=ci", "change:version": "pnpm chronus version --ignore-policies --only @typespec/http-client-python", "change:add": "pnpm chronus add", diff --git a/packages/http-client-python/tests/conftest.py b/packages/http-client-python/tests/conftest.py index d3780e9e216..a0badda9ef7 100644 --- a/packages/http-client-python/tests/conftest.py +++ b/packages/http-client-python/tests/conftest.py @@ -28,10 +28,6 @@ LOCK_FILE = Path(tempfile.gettempdir()) / "http_client_python_test_server.lock" PID_FILE = Path(tempfile.gettempdir()) / "http_client_python_test_server.pid" -# Global server process reference (used by hooks) -_server_process = None -_owns_server = False # Track if this process started the server - def wait_for_server(url: str, timeout: int = 60, interval: float = 0.5) -> bool: """Wait for the server to be ready by polling the URL.""" @@ -50,34 +46,29 @@ def wait_for_server(url: str, timeout: int = 60, interval: float = 0.5) -> bool: def start_server_process(): - """Start the tsp-spector mock API server.""" + """Start the tsp-spector mock API server. + + Always serves both azure-http-specs and http-specs regardless of flavor. + This allows azure and unbranded tests to run in parallel using the same server. + """ azure_http_path = ROOT / "node_modules/@azure-tools/azure-http-specs" http_path = ROOT / "node_modules/@typespec/http-specs" - # Determine flavor from environment or current directory - flavor = os.environ.get("FLAVOR", "azure") - + # Always serve both spec sets so azure and unbranded tests can run in parallel # Use absolute paths with forward slashes (works on all platforms including Windows) - if flavor == "unbranded": - cwd = http_path.resolve() - specs_path = str(cwd / "specs").replace("\\", "/") - cmd = f"npx tsp-spector serve {specs_path}" - else: - cwd = azure_http_path.resolve() - azure_specs = str(cwd / "specs").replace("\\", "/") - http_specs = str((http_path / "specs").resolve()).replace("\\", "/") - cmd = f"npx tsp-spector serve {azure_specs} {http_specs}" + cwd = azure_http_path.resolve() + azure_specs = str(cwd / "specs").replace("\\", "/") + http_specs = str((http_path / "specs").resolve()).replace("\\", "/") + cmd = f"npx tsp-spector serve {azure_specs} {http_specs}" # Add node_modules/.bin to PATH env = os.environ.copy() node_bin = str(ROOT / "node_modules" / ".bin") env["PATH"] = f"{node_bin}{os.pathsep}{env.get('PATH', '')}" - # Suppress server stdout/stderr to avoid confusing "Request validation failed" warnings - # in test output. Server readiness is validated via HTTP polling in wait_for_server(). if os.name == "nt": - return subprocess.Popen(cmd, shell=True, cwd=str(cwd), env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - return subprocess.Popen(cmd, shell=True, cwd=str(cwd), env=env, preexec_fn=os.setsid, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + return subprocess.Popen(cmd, shell=True, cwd=str(cwd), env=env) + return subprocess.Popen(cmd, shell=True, cwd=str(cwd), env=env, preexec_fn=os.setsid) def terminate_server_process(process): @@ -105,91 +96,35 @@ def terminate_server_process(process): pass -def pytest_configure(config): - """Start the mock server before any tests run. - - Uses file locking to ensure only one process starts the server, - even when running with pytest-xdist. The controller process starts - the server and workers wait for it to be ready. - """ - global _server_process, _owns_server - - # Check if server is already running (e.g., from a previous run or external process) - if wait_for_server(SERVER_URL, timeout=1, interval=0.1): - print(f"Mock API server already running at {SERVER_URL}") - return - - # Use file lock to ensure only one process starts the server - # This handles both xdist workers and multiple test runs - lock = FileLock(str(LOCK_FILE), timeout=120) - - try: - with lock: - # Double-check after acquiring lock (another process may have started it) - if wait_for_server(SERVER_URL, timeout=1, interval=0.1): - print(f"Mock API server already running at {SERVER_URL}") - return - - # We're the first process - start the server - print(f"Starting mock API server...") - _server_process = start_server_process() - _owns_server = True - - # Check if process started successfully - if _server_process.poll() is not None: - pytest.exit(f"Mock API server process exited immediately with code {_server_process.returncode}") - - # Write PID file so other processes know who owns the server - PID_FILE.write_text(str(_server_process.pid)) - - # Wait for server to be ready - if not wait_for_server(SERVER_URL, timeout=60): - if _server_process.poll() is not None: - pytest.exit(f"Mock API server process died with code {_server_process.returncode}") - terminate_server_process(_server_process) - _server_process = None - _owns_server = False - pytest.exit(f"Mock API server failed to start within 60 seconds at {SERVER_URL}") - - print(f"Mock API server ready at {SERVER_URL}") - - except TimeoutError: - # Another process is holding the lock for too long - # Check if server is available anyway - if wait_for_server(SERVER_URL, timeout=5): - print(f"Mock API server available at {SERVER_URL} (started by another process)") - else: - pytest.exit("Timeout waiting for server lock - another process may be stuck") - - -def pytest_unconfigure(config): - """Stop the mock server after all tests complete.""" - global _server_process, _owns_server - - # Only stop the server if this process started it - if not _owns_server: - return - - terminate_server_process(_server_process) - _server_process = None - _owns_server = False - - # Clean up PID file - try: - PID_FILE.unlink(missing_ok=True) - except Exception: - pass - - @pytest.fixture(scope="session", autouse=True) -def testserver(request): - """Ensure the mock server is ready before tests run. +def testserver(): + """Start the mock API server, coordinated across xdist workers via file lock. - The server is started in pytest_configure (controller process). - This fixture just verifies the server is accessible from workers. + The first process to acquire the lock starts the server; others wait for it. + The server is intentionally NOT killed in teardown — with xdist, the owning + worker may finish before others, killing the server prematurely. The server + is cleaned up when the tox/parent process exits. """ + # Check if server is already running + if not wait_for_server(SERVER_URL, timeout=1, interval=0.1): + lock = FileLock(str(LOCK_FILE), timeout=120) + try: + with lock: + # Double-check after acquiring lock + if not wait_for_server(SERVER_URL, timeout=1, interval=0.1): + server = start_server_process() + PID_FILE.write_text(str(server.pid)) + if not wait_for_server(SERVER_URL, timeout=60): + terminate_server_process(server) + pytest.fail(f"Mock API server failed to start at {SERVER_URL}") + except TimeoutError: + if not wait_for_server(SERVER_URL, timeout=5): + pytest.fail("Timeout waiting for server lock") + + # Final check that server is reachable if not wait_for_server(SERVER_URL, timeout=30): pytest.fail(f"Mock API server not available at {SERVER_URL}") + yield diff --git a/packages/http-client-python/tests/install_packages.py b/packages/http-client-python/tests/install_packages.py index f706ae93732..ae7862e33fd 100644 --- a/packages/http-client-python/tests/install_packages.py +++ b/packages/http-client-python/tests/install_packages.py @@ -1,8 +1,12 @@ #!/usr/bin/env python """Install generated packages for testing. -This script handles cross-platform path issues that can occur with inline -tox commands on Windows. +Supports two modes: +1. Build wheels from source dirs into a wheel directory (build command) +2. Install from pre-built wheels via --find-links (instant, no compilation) + +The build step runs once before tox envs start. Each tox env then installs +from pre-built wheels, avoiding redundant source builds across environments. """ import glob @@ -11,57 +15,137 @@ import sys -def install_packages(flavor: str, tests_dir: str) -> None: +def _find_packages(generated_dir): + """Find all package directories that have pyproject.toml or setup.py.""" + all_dirs = glob.glob(os.path.join(generated_dir, "*")) + return sorted([ + p for p in all_dirs + if os.path.isdir(p) and ( + os.path.exists(os.path.join(p, "pyproject.toml")) or + os.path.exists(os.path.join(p, "setup.py")) + ) + ]) + + +def build_wheels(flavor, tests_dir): + """Build wheels for all packages into a shared directory.""" + generated_dir = os.path.join(tests_dir, "generated", flavor) + wheel_dir = os.path.join(tests_dir, ".wheels", flavor) + os.makedirs(wheel_dir, exist_ok=True) + + packages = _find_packages(generated_dir) + if not packages: + print(f"Warning: No packages found in {generated_dir}") + return + + print(f"Building {len(packages)} wheels for {flavor}...") + + batch_size = 50 + for i in range(0, len(packages), batch_size): + batch = packages[i:i + batch_size] + try: + subprocess.run( + ["uv", "pip", "wheel", "--no-deps", "--wheel-dir", wheel_dir] + batch, + check=True, + ) + except FileNotFoundError: + subprocess.run( + [sys.executable, "-m", "pip", "wheel", "--no-deps", "--wheel-dir", wheel_dir] + batch, + check=True, + ) + + wheel_count = len(glob.glob(os.path.join(wheel_dir, "*.whl"))) + print(f"Built {wheel_count} wheels for {flavor}") + + +def install_packages(flavor, tests_dir): """Install generated packages for the given flavor.""" generated_dir = os.path.join(tests_dir, "generated", flavor) + wheel_dir = os.path.join(tests_dir, ".wheels", flavor) if not os.path.exists(generated_dir): print(f"Warning: Generated directory does not exist: {generated_dir}") return - # Find all package directories - packages = glob.glob(os.path.join(generated_dir, "*")) - packages = [p for p in packages if os.path.isdir(p)] - + packages = _find_packages(generated_dir) if not packages: print(f"Warning: No packages found in {generated_dir}") return print(f"Installing {len(packages)} packages from {generated_dir}") - # Install packages using uv pip - # Use --no-deps to avoid dependency resolution overhead - cmd = ["uv", "pip", "install", "--no-deps"] + packages - - try: - subprocess.run(cmd, check=True) - print(f"Successfully installed {len(packages)} packages") - except subprocess.CalledProcessError as e: - print(f"Error installing packages: {e}") - sys.exit(1) - except FileNotFoundError: - # uv not found, try pip - print("uv not found, falling back to pip") - cmd = [sys.executable, "-m", "pip", "install", "--no-deps"] + packages - subprocess.run(cmd, check=True) + use_wheels = os.path.isdir(wheel_dir) and bool(glob.glob(os.path.join(wheel_dir, "*.whl"))) + use_uv = True + + if use_wheels: + # Install from pre-built wheels (instant, no compilation). + # Use wheel filenames to derive package specs since --no-index + # won't resolve source directory paths. + wheel_files = glob.glob(os.path.join(wheel_dir, "*.whl")) + print(f" Using {len(wheel_files)} pre-built wheels from .wheels/{flavor}/") + try: + cmd = ["uv", "pip", "install", "--no-deps", "--no-index", "--find-links", wheel_dir] + wheel_files + subprocess.run(cmd, check=True) + print(f"Successfully installed {len(wheel_files)} packages") + return + except FileNotFoundError: + use_uv = False + try: + cmd = [sys.executable, "-m", "pip", "install", "--no-deps", "--no-index", "--find-links", wheel_dir] + wheel_files + subprocess.run(cmd, check=True) + print(f"Successfully installed {len(wheel_files)} packages") + return + except subprocess.CalledProcessError: + print(" Wheel install failed, falling back to source install") + except subprocess.CalledProcessError: + print(" Wheel install failed, falling back to source install") + + # Fall back to source install with per-flavor cache + cache_dir = os.path.join(tests_dir, ".uv-cache", flavor) + batch_size = 50 + for i in range(0, len(packages), batch_size): + batch = packages[i:i + batch_size] + if use_uv: + cmd = ["uv", "pip", "install", "--no-deps", "--cache-dir", cache_dir] + batch + else: + cmd = [sys.executable, "-m", "pip", "install", "--no-deps"] + batch + try: + subprocess.run(cmd, check=True) + except subprocess.CalledProcessError as e: + print(f"Error installing packages: {e}") + sys.exit(1) + except FileNotFoundError: + if use_uv: + print("uv not found, falling back to pip") + use_uv = False + cmd = [sys.executable, "-m", "pip", "install", "--no-deps"] + batch + subprocess.run(cmd, check=True) + + print(f"Successfully installed {len(packages)} packages") def main(): if len(sys.argv) < 2: print("Usage: install_packages.py [tests_dir]") - print(" flavor: azure or unbranded") - print(" tests_dir: optional, defaults to script directory") + print(" install_packages.py build [tests_dir]") sys.exit(1) - flavor = sys.argv[1] - tests_dir = sys.argv[2] if len(sys.argv) > 2 else os.path.dirname(os.path.abspath(__file__)) - - if flavor not in ("azure", "unbranded"): - print(f"Error: Invalid flavor '{flavor}'. Must be 'azure' or 'unbranded'") + # Support both old-style (install_packages.py ) and new build command + if sys.argv[1] == "build": + if len(sys.argv) < 3: + print("Usage: install_packages.py build [tests_dir]") + sys.exit(1) + flavor = sys.argv[2] + tests_dir = sys.argv[3] if len(sys.argv) > 3 else os.path.dirname(os.path.abspath(__file__)) + build_wheels(flavor, tests_dir) + elif sys.argv[1] in ("azure", "unbranded"): + flavor = sys.argv[1] + tests_dir = sys.argv[2] if len(sys.argv) > 2 else os.path.dirname(os.path.abspath(__file__)) + install_packages(flavor, tests_dir) + else: + print(f"Error: Unknown command or flavor '{sys.argv[1]}'") sys.exit(1) - install_packages(flavor, tests_dir) - if __name__ == "__main__": main() diff --git a/packages/http-client-python/tests/mock_api/azure/conftest.py b/packages/http-client-python/tests/mock_api/azure/conftest.py index 29824951ef4..20d7152e5d4 100644 --- a/packages/http-client-python/tests/mock_api/azure/conftest.py +++ b/packages/http-client-python/tests/mock_api/azure/conftest.py @@ -3,9 +3,6 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -import os -import subprocess -import signal import pytest import re from typing import Literal @@ -14,35 +11,6 @@ FILE_FOLDER = Path(__file__).parent -def start_server_process(): - azure_http_path = Path(os.path.dirname(__file__)) / Path("../../../node_modules/@azure-tools/azure-http-specs") - http_path = Path(os.path.dirname(__file__)) / Path("../../../node_modules/@typespec/http-specs") - os.chdir(azure_http_path.resolve()) - cmd = f"npx tsp-spector serve ./specs {(http_path / 'specs').resolve()}" - if os.name == "nt": - return subprocess.Popen(cmd, shell=True) - return subprocess.Popen(cmd, shell=True, preexec_fn=os.setsid) - - -def terminate_server_process(process): - try: - if os.name == "nt": - process.kill() - else: - os.killpg(os.getpgid(process.pid), signal.SIGTERM) # Send the signal to all the process groups - except ProcessLookupError: - # Process already terminated, which is fine - pass - - -@pytest.fixture(scope="session", autouse=True) -def testserver(): - """Start spector ranch mock api tests""" - server = start_server_process() - yield - terminate_server_process(server) - - _VALID_UUID = re.compile(r"^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$") _VALID_RFC7231 = re.compile( r"^(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s\d{2}\s" diff --git a/packages/http-client-python/tests/mock_api/shared/conftest.py b/packages/http-client-python/tests/mock_api/shared/conftest.py index 727f986ae44..0f5685c338c 100644 --- a/packages/http-client-python/tests/mock_api/shared/conftest.py +++ b/packages/http-client-python/tests/mock_api/shared/conftest.py @@ -3,9 +3,6 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -import os -import subprocess -import signal import pytest import importlib from pathlib import Path @@ -13,39 +10,6 @@ DATA_FOLDER = Path(__file__).parent.parent -def start_server_process(): - azure_http_path = Path(os.path.dirname(__file__)) / Path("../../../node_modules/@azure-tools/azure-http-specs") - http_path = Path(os.path.dirname(__file__)) / Path("../../../node_modules/@typespec/http-specs") - if "unbranded" in Path(os.getcwd()).parts: - os.chdir(http_path.resolve()) - cmd = "npx tsp-spector serve ./specs" - else: - os.chdir(azure_http_path.resolve()) - cmd = f"npx tsp-spector serve ./specs {(http_path / 'specs').resolve()}" - if os.name == "nt": - return subprocess.Popen(cmd, shell=True) - return subprocess.Popen(cmd, shell=True, preexec_fn=os.setsid) - - -def terminate_server_process(process): - try: - if os.name == "nt": - process.kill() - else: - os.killpg(os.getpgid(process.pid), signal.SIGTERM) # Send the signal to all the process groups - except ProcessLookupError: - # Process already terminated, which is fine - pass - - -@pytest.fixture(scope="session", autouse=True) -def testserver(): - """Start spector mock api tests""" - server = start_server_process() - yield - terminate_server_process(server) - - """ Use to disambiguate the core library we use """ diff --git a/packages/http-client-python/tests/mock_api/unbranded/conftest.py b/packages/http-client-python/tests/mock_api/unbranded/conftest.py index 5d190ae0bd4..df7cb9efbea 100644 --- a/packages/http-client-python/tests/mock_api/unbranded/conftest.py +++ b/packages/http-client-python/tests/mock_api/unbranded/conftest.py @@ -3,44 +3,12 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -import os -import subprocess -import signal import pytest -import re from pathlib import Path FILE_FOLDER = Path(__file__).parent -def start_server_process(): - http_path = Path(os.path.dirname(__file__)) / Path("../../../node_modules/@typespec/http-specs") - os.chdir(http_path.resolve()) - cmd = "tsp-spector serve ./specs" - if os.name == "nt": - return subprocess.Popen(cmd, shell=True) - return subprocess.Popen(cmd, shell=True, preexec_fn=os.setsid) - - -def terminate_server_process(process): - try: - if os.name == "nt": - process.kill() - else: - os.killpg(os.getpgid(process.pid), signal.SIGTERM) # Send the signal to all the process groups - except ProcessLookupError: - # Process already terminated, which is fine - pass - - -@pytest.fixture(scope="session", autouse=True) -def testserver(): - """Start spector mock api tests""" - server = start_server_process() - yield - terminate_server_process(server) - - SPECIAL_WORDS = [ "and", "as", diff --git a/packages/http-client-python/tests/mock_api/unbranded/test_unbranded.py b/packages/http-client-python/tests/mock_api/unbranded/test_unbranded.py index f7366edc0a5..60d537d3293 100644 --- a/packages/http-client-python/tests/mock_api/unbranded/test_unbranded.py +++ b/packages/http-client-python/tests/mock_api/unbranded/test_unbranded.py @@ -31,7 +31,7 @@ def test_track_back(client: ScalarClient): assert "microsoft" not in track_back -_SKIP_DIRS = {"__pycache__", "pytest_cache", ".pytest_cache"} +_SKIP_DIRS = {"__pycache__", "pytest_cache", ".pytest_cache", "generated_tests"} def check_sensitive_word(folder: Path, word: str) -> list[str]: diff --git a/packages/http-client-python/tests/requirements/docs.txt b/packages/http-client-python/tests/requirements/docs.txt index 7839a0e726f..5b80499ba34 100644 --- a/packages/http-client-python/tests/requirements/docs.txt +++ b/packages/http-client-python/tests/requirements/docs.txt @@ -1,6 +1,8 @@ # Documentation dependencies -r base.txt pip +pylint +pkginfo sphinx>=7.0.0 sphinx_rtd_theme>=2.0.0 myst_parser>=2.0.0 diff --git a/packages/http-client-python/tests/requirements/lint.txt b/packages/http-client-python/tests/requirements/lint.txt index 2a9896f8d75..736a7806543 100644 --- a/packages/http-client-python/tests/requirements/lint.txt +++ b/packages/http-client-python/tests/requirements/lint.txt @@ -1,4 +1,4 @@ # Linting dependencies -r base.txt pylint==4.0.4 -black==24.8.0 +black==26.3.1 diff --git a/packages/http-client-python/tests/tox.ini b/packages/http-client-python/tests/tox.ini index 12231969731..a94a60603e4 100644 --- a/packages/http-client-python/tests/tox.ini +++ b/packages/http-client-python/tests/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = test-{azure,unbranded}, lint-{azure,unbranded}, mypy-{azure,unbranded}, pyright-{azure,unbranded}, unittest +envlist = test-{azure,unbranded}, check-{azure,unbranded}, docs-{azure,unbranded}, unittest skipsdist = True isolated_build = True requires = tox-uv @@ -28,7 +28,7 @@ deps = -e {tox_root}/../generator commands = python {tox_root}/install_packages.py azure {tox_root} - pytest mock_api/azure mock_api/shared -v -n auto -n auto {posargs} + pytest mock_api/azure mock_api/shared -v -n auto {posargs} [testenv:test-unbranded] description = Run tests for unbranded flavor @@ -41,7 +41,7 @@ deps = -e {tox_root}/../generator commands = python {tox_root}/install_packages.py unbranded {tox_root} - pytest mock_api/unbranded mock_api/shared -v -n auto -n auto {posargs} + pytest mock_api/unbranded mock_api/shared -v -n auto {posargs} [testenv:unittest] description = Run unit tests for pygen internals @@ -54,11 +54,13 @@ commands = pytest unit/ -v -n auto {posargs} # ============================================================================= -# Lint environments +# Lint, type checking, and static analysis environments +# Split into separate envs for maximum parallelism. Pre-built wheels make +# per-env package installs cheap (~5s instead of ~2min). # ============================================================================= [testenv:lint-azure] -description = Run linting for Azure flavor +description = Run pylint for Azure flavor setenv = {[testenv]setenv} FLAVOR = azure @@ -72,7 +74,7 @@ commands = python {tox_root}/../eng/scripts/ci/run_pylint.py -t azure -s generated {posargs} [testenv:lint-unbranded] -description = Run linting for unbranded flavor +description = Run pylint for unbranded flavor setenv = {[testenv]setenv} FLAVOR = unbranded @@ -84,10 +86,6 @@ commands = python {tox_root}/install_packages.py unbranded {tox_root} python {tox_root}/../eng/scripts/ci/run_pylint.py -t unbranded -s generated {posargs} -# ============================================================================= -# Type checking environments (separate mypy and pyright) -# ============================================================================= - [testenv:mypy-azure] description = Run mypy type checking for Azure flavor setenv = @@ -95,6 +93,7 @@ setenv = FLAVOR = azure deps = -r {tox_root}/requirements/typecheck.txt + -r {tox_root}/requirements/azure.txt -e {tox_root}/../generator commands = python {tox_root}/install_packages.py azure {tox_root} @@ -107,6 +106,7 @@ setenv = FLAVOR = unbranded deps = -r {tox_root}/requirements/typecheck.txt + -r {tox_root}/requirements/unbranded.txt -e {tox_root}/../generator commands = python {tox_root}/install_packages.py unbranded {tox_root} @@ -119,6 +119,7 @@ setenv = FLAVOR = azure deps = -r {tox_root}/requirements/typecheck.txt + -r {tox_root}/requirements/azure.txt -e {tox_root}/../generator commands = python {tox_root}/install_packages.py azure {tox_root} @@ -131,17 +132,18 @@ setenv = FLAVOR = unbranded deps = -r {tox_root}/requirements/typecheck.txt + -r {tox_root}/requirements/unbranded.txt -e {tox_root}/../generator commands = python {tox_root}/install_packages.py unbranded {tox_root} python {tox_root}/../eng/scripts/ci/run_pyright.py -t unbranded -s generated {posargs} # ============================================================================= -# Documentation environments +# Documentation environments (apiview and sphinx split for parallelism) # ============================================================================= -[testenv:docs-azure] -description = Run documentation validation for Azure flavor +[testenv:apiview-azure] +description = Run apiview validation for Azure flavor basepython = python3.10 setenv = {[testenv]setenv} @@ -150,13 +152,13 @@ deps = -r {tox_root}/requirements/docs.txt -e {tox_root}/../generator commands = - uv pip install apiview-stub-generator>=0.3.19 --index-url="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" + uv pip install apiview-stub-generator>=0.3.19 pylint-guidelines-checker --no-deps --index-url="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" + uv pip install astroid charset-normalizer pylint pkginfo python {tox_root}/install_packages.py azure {tox_root} python {tox_root}/../eng/scripts/ci/run_apiview.py -t azure -s generated {posargs} - python {tox_root}/../eng/scripts/ci/run_sphinx_build.py -t azure -s generated {posargs} -[testenv:docs-unbranded] -description = Run documentation validation for unbranded flavor +[testenv:apiview-unbranded] +description = Run apiview validation for unbranded flavor basepython = python3.10 setenv = {[testenv]setenv} @@ -165,45 +167,35 @@ deps = -r {tox_root}/requirements/docs.txt -e {tox_root}/../generator commands = - uv pip install apiview-stub-generator>=0.3.19 --index-url="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" + uv pip install apiview-stub-generator>=0.3.19 pylint-guidelines-checker --no-deps --index-url="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" + uv pip install astroid charset-normalizer pylint pkginfo python {tox_root}/install_packages.py unbranded {tox_root} python {tox_root}/../eng/scripts/ci/run_apiview.py -t unbranded -s generated {posargs} - python {tox_root}/../eng/scripts/ci/run_sphinx_build.py -t unbranded -s generated {posargs} - -# ============================================================================= -# CI environments (combines all checks) -# ============================================================================= -[testenv:ci-azure] -description = Run full CI for Azure flavor +[testenv:sphinx-azure] +description = Run sphinx docstring validation for Azure flavor +basepython = python3.10 setenv = {[testenv]setenv} FLAVOR = azure deps = - -r {tox_root}/requirements/lint.txt - -r {tox_root}/requirements/typecheck.txt + -r {tox_root}/requirements/docs.txt -r {tox_root}/requirements/azure.txt -e {tox_root}/../generator commands = python {tox_root}/install_packages.py azure {tox_root} - pytest mock_api/azure mock_api/shared -v -n auto - python {tox_root}/../eng/scripts/ci/run_pylint.py -t azure -s generated - python {tox_root}/../eng/scripts/ci/run_mypy.py -t azure -s generated - python {tox_root}/../eng/scripts/ci/run_pyright.py -t azure -s generated + python {tox_root}/../eng/scripts/ci/run_sphinx_build.py -t azure -s generated {posargs} -[testenv:ci-unbranded] -description = Run full CI for unbranded flavor +[testenv:sphinx-unbranded] +description = Run sphinx docstring validation for unbranded flavor +basepython = python3.10 setenv = {[testenv]setenv} FLAVOR = unbranded deps = - -r {tox_root}/requirements/lint.txt - -r {tox_root}/requirements/typecheck.txt + -r {tox_root}/requirements/docs.txt -r {tox_root}/requirements/unbranded.txt -e {tox_root}/../generator commands = python {tox_root}/install_packages.py unbranded {tox_root} - pytest mock_api/unbranded mock_api/shared -v -n auto - python {tox_root}/../eng/scripts/ci/run_pylint.py -t unbranded -s generated - python {tox_root}/../eng/scripts/ci/run_mypy.py -t unbranded -s generated - python {tox_root}/../eng/scripts/ci/run_pyright.py -t unbranded -s generated + python {tox_root}/../eng/scripts/ci/run_sphinx_build.py -t unbranded -s generated {posargs} diff --git a/packages/playground/src/react/output-view/file-viewer.tsx b/packages/playground/src/react/output-view/file-viewer.tsx index 28bb2ba1f01..eda1dc83403 100644 --- a/packages/playground/src/react/output-view/file-viewer.tsx +++ b/packages/playground/src/react/output-view/file-viewer.tsx @@ -14,14 +14,12 @@ const FileViewerComponent = ({ program, outputFiles, fileViewers, - highlightChanges, }: OutputViewerProps & { fileViewers: Record; - highlightChanges: boolean; }) => { const [filename, setFilename] = useState(""); const [content, setContent] = useState(""); - const { changedFiles, changedLines } = useFileChanges(program, outputFiles, highlightChanges); + const { changedFiles, changedLines } = useFileChanges(program, outputFiles); const showFileTree = useMemo( () => outputFiles.some((f) => f.includes("/")) || outputFiles.length >= 3, @@ -71,7 +69,7 @@ const FileViewerComponent = ({ files={outputFiles} selected={filename} onSelect={handleFileSelection} - changedFiles={highlightChanges ? changedFiles : undefined} + changedFiles={changedFiles} /> @@ -82,7 +80,7 @@ const FileViewerComponent = ({ filename={filename} content={content} viewers={fileViewers} - changedLineNumbers={highlightChanges ? changedLines.get(filename) : undefined} + changedLineNumbers={changedLines.get(filename)} /> @@ -100,36 +98,21 @@ const FileViewerComponent = ({ filename={filename} content={content} viewers={fileViewers} - changedLineNumbers={highlightChanges ? changedLines.get(filename) : undefined} + changedLineNumbers={changedLines.get(filename)} /> ); }; -export interface FileViewerOptions { - /** When true, highlights changed files in the tree and changed lines in the editor after recompilation. */ - highlightChanges?: boolean; -} - -export function createFileViewer( - fileViewers: FileOutputViewer[], - options?: FileViewerOptions, -): ProgramViewer { +export function createFileViewer(fileViewers: FileOutputViewer[]): ProgramViewer { const viewerMap = Object.fromEntries(fileViewers.map((x) => [x.key, x])); - const highlightChanges = options?.highlightChanges ?? false; return { key: "file-output", label: "Output explorer", icon: , render: (props) => { - return ( - - ); + return ; }, }; } diff --git a/packages/playground/src/react/output-view/output-view.tsx b/packages/playground/src/react/output-view/output-view.tsx index 2e644e5cce8..001538c9599 100644 --- a/packages/playground/src/react/output-view/output-view.tsx +++ b/packages/playground/src/react/output-view/output-view.tsx @@ -25,10 +25,6 @@ export interface OutputViewProps { */ viewers?: ProgramViewer[]; fileViewers?: FileOutputViewer[]; - /** - * When true, highlights changed files and lines after recompilation. - */ - highlightChanges?: boolean; /** * The currently selected viewer key. */ @@ -53,15 +49,14 @@ export const OutputView: FunctionComponent = ({ isOutputStale, viewers, fileViewers, - highlightChanges, selectedViewer, onViewerChange, viewerState, onViewerStateChange, }) => { const resolvedViewers = useMemo( - () => resolveViewers(viewers, fileViewers, highlightChanges), - [fileViewers, viewers, highlightChanges], + () => resolveViewers(viewers, fileViewers), + [fileViewers, viewers], ); if (compilationState === undefined) { @@ -104,9 +99,8 @@ export const OutputView: FunctionComponent = ({ function resolveViewers( viewers: ProgramViewer[] | undefined, fileViewers: FileOutputViewer[] | undefined, - highlightChanges?: boolean, ): ResolvedViewers { - const fileViewer = createFileViewer(fileViewers ?? [], { highlightChanges }); + const fileViewer = createFileViewer(fileViewers ?? []); const output: ResolvedViewers = { programViewers: { [fileViewer.key]: fileViewer, diff --git a/packages/playground/src/react/output-view/use-file-changes.ts b/packages/playground/src/react/output-view/use-file-changes.ts index ec814da7fd3..650a5816a2f 100644 --- a/packages/playground/src/react/output-view/use-file-changes.ts +++ b/packages/playground/src/react/output-view/use-file-changes.ts @@ -14,14 +14,12 @@ export interface FileChanges { export function useFileChanges( program: CompileResult["program"], outputFiles: string[], - highlightChanges: boolean, ): FileChanges { const [changedFiles, setChangedFiles] = useState>(new Set()); const [changedLines, setChangedLines] = useState>(new Map()); const prevContentsRef = useRef>(new Map()); useEffect(() => { - if (!highlightChanges) return; let cancelled = false; async function diffFiles() { const changed = new Set(); @@ -74,7 +72,7 @@ export function useFileChanges( return () => { cancelled = true; }; - }, [program, outputFiles, highlightChanges]); + }, [program, outputFiles]); return useMemo(() => ({ changedFiles, changedLines }), [changedFiles, changedLines]); } diff --git a/packages/playground/src/react/playground.tsx b/packages/playground/src/react/playground.tsx index b65bb51b421..956c49e9a74 100644 --- a/packages/playground/src/react/playground.tsx +++ b/packages/playground/src/react/playground.tsx @@ -38,8 +38,6 @@ export type { PlaygroundState }; export interface PlaygroundEmitterOptions { /** Compile debounce delay in milliseconds. Default is 200. */ debounce?: number; - /** When true, highlights changed files and lines after recompilation. */ - newChangeDiff?: boolean; } export interface PlaygroundProps { @@ -227,12 +225,23 @@ export const Playground: FunctionComponent = (props) => { }, [content, selectedSampleName, props.samples]); const compileIdRef = useRef(0); + const isCompilingRef = useRef(false); + const pendingRecompileRef = useRef(false); + const doCompileRef = useRef<() => Promise>(() => Promise.resolve()); const doCompile = useCallback(async () => { + // If a compile is already in progress, mark that a recompile is needed and + // bail out. The in-flight compile will re-trigger on completion. This avoids + // stacking up synchronous compiles that block the UI thread during typing. + if (isCompilingRef.current) { + pendingRecompileRef.current = true; + return; + } const currentContent = typespecModel.getValue(); const typespecCompiler = host.compiler; const compileId = ++compileIdRef.current; + isCompilingRef.current = true; setIsCompiling(true); let state: CompilationState; try { @@ -240,10 +249,16 @@ export const Playground: FunctionComponent = (props) => { } catch (error) { // eslint-disable-next-line no-console console.error("Compilation failed", error); - return; - } finally { + isCompilingRef.current = false; setIsCompiling(false); + if (pendingRecompileRef.current) { + pendingRecompileRef.current = false; + void doCompileRef.current(); + } + return; } + isCompilingRef.current = false; + setIsCompiling(false); // Discard stale results from an older compilation if (compileId !== compileIdRef.current) return; @@ -289,8 +304,19 @@ export const Playground: FunctionComponent = (props) => { updateDiagnosticsForCodeFixes(typespecCompiler, []); editor.setModelMarkers(typespecModel, "owner", []); } + + // If typing happened while this compile was running, trigger a trailing + // compile so the output stays in sync with the latest content. + if (pendingRecompileRef.current) { + pendingRecompileRef.current = false; + void doCompileRef.current(); + } }, [host, selectedEmitter, compilerOptions, typespecModel]); + useEffect(() => { + doCompileRef.current = doCompile; + }, [doCompile]); + const currentEmitterOptions = selectedEmitter ? props.emitterOptions?.[selectedEmitter] : undefined; @@ -438,7 +464,6 @@ export const Playground: FunctionComponent = (props) => { editorOptions={props.editorOptions} viewers={props.viewers} fileViewers={selectedEmitter ? props.emitterViewers?.[selectedEmitter] : undefined} - highlightChanges={currentEmitterOptions?.newChangeDiff} selectedViewer={selectedViewer} onViewerChange={onSelectedViewerChange} viewerState={viewerState} diff --git a/packages/playground/src/react/typespec-editor.css b/packages/playground/src/react/typespec-editor.css new file mode 100644 index 00000000000..8aa4ad6581a --- /dev/null +++ b/packages/playground/src/react/typespec-editor.css @@ -0,0 +1,4 @@ +/* Monaco decoration class (must be a plain global selector) */ +.playground-changed-line { + background-color: rgba(0, 180, 0, 0.15); +} diff --git a/packages/playground/src/react/typespec-editor.module.css b/packages/playground/src/react/typespec-editor.module.css deleted file mode 100644 index 2caf0277e74..00000000000 --- a/packages/playground/src/react/typespec-editor.module.css +++ /dev/null @@ -1,4 +0,0 @@ -/* Monaco decorations require global class names */ -:global(.playground-changed-line) { - background-color: rgba(0, 180, 0, 0.15); -} diff --git a/packages/playground/src/react/typespec-editor.tsx b/packages/playground/src/react/typespec-editor.tsx index 65449dfec7f..a5e40613fa1 100644 --- a/packages/playground/src/react/typespec-editor.tsx +++ b/packages/playground/src/react/typespec-editor.tsx @@ -2,7 +2,7 @@ import { editor, Range } from "monaco-editor"; import { useCallback, useEffect, useRef, useState, type FunctionComponent } from "react"; import { Editor, useMonacoModel, type EditorProps } from "./editor.js"; import type { PlaygroundEditorsOptions } from "./playground.js"; -import "./typespec-editor.module.css"; +import "./typespec-editor.css"; // Re-export for backward compatibility export { getChangedLineNumbers } from "./diff-utils.js"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c05fffed13a..f81f1fe387e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -211,8 +211,8 @@ catalogs: specifier: ^4.1.3 version: 4.1.3 '@vitest/eslint-plugin': - specifier: ^1.6.14 - version: 1.6.14 + specifier: ^1.6.16 + version: 1.6.16 '@vitest/ui': specifier: ^4.1.3 version: 4.1.3 @@ -250,8 +250,8 @@ catalogs: specifier: ^3.0.1 version: 3.0.1 astro: - specifier: ^6.1.5 - version: 6.1.5 + specifier: ^6.1.8 + version: 6.1.8 astro-expressive-code: specifier: ^0.41.7 version: 0.41.7 @@ -319,11 +319,11 @@ catalogs: specifier: ^5.2.1 version: 5.2.1 fast-xml-parser: - specifier: ^5.5.9 - version: 5.5.10 + specifier: ^5.7.0 + version: 5.7.1 happy-dom: - specifier: ^20.8.9 - version: 20.8.9 + specifier: ^20.9.0 + version: 20.9.0 is-unicode-supported: specifier: ^2.1.0 version: 2.1.0 @@ -562,7 +562,7 @@ importers: version: 4.1.3(vitest@4.1.3) '@vitest/eslint-plugin': specifier: 'catalog:' - version: 1.6.14(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2)(vitest@4.1.3) + version: 1.6.16(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2)(vitest@4.1.3) c8: specifier: 'catalog:' version: 11.0.0 @@ -613,7 +613,7 @@ importers: version: 8.58.1(eslint@10.2.0)(typescript@6.0.2) vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) yaml: specifier: 'catalog:' version: 2.8.3 @@ -642,7 +642,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/astro-utils: dependencies: @@ -651,7 +651,7 @@ importers: version: 0.9.8(prettier-plugin-astro@0.14.1)(prettier@3.8.1)(typescript@6.0.2) '@astrojs/starlight': specifier: 'catalog:' - version: 0.38.3(astro@6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + version: 0.38.3(astro@6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) '@expressive-code/core': specifier: 'catalog:' version: 0.41.7 @@ -660,7 +660,7 @@ importers: version: link:../playground astro-expressive-code: specifier: 'catalog:' - version: 0.41.7(astro@6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + version: 0.41.7(astro@6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) pathe: specifier: 'catalog:' version: 2.0.3 @@ -676,7 +676,7 @@ importers: version: 19.2.14 astro: specifier: 'catalog:' - version: 6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) packages/best-practices: devDependencies: @@ -700,7 +700,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/bundle-uploader: dependencies: @@ -743,7 +743,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/bundler: dependencies: @@ -786,7 +786,7 @@ importers: version: 8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/compiler: dependencies: @@ -880,7 +880,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vscode-oniguruma: specifier: 'catalog:' version: 2.0.1 @@ -941,7 +941,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) web-tree-sitter: specifier: 'catalog:' version: 0.26.8 @@ -981,7 +981,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/events: devDependencies: @@ -1011,7 +1011,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/html-program-viewer: dependencies: @@ -1087,7 +1087,7 @@ importers: version: 4.5.4(@types/node@25.5.2)(rollup@4.60.1)(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/http: devDependencies: @@ -1120,7 +1120,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/http-canonicalization: dependencies: @@ -1184,7 +1184,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/http-client-js: dependencies: @@ -1272,7 +1272,7 @@ importers: version: 2.0.0 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) yargs: specifier: 'catalog:' version: 18.0.0 @@ -1363,7 +1363,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/http-server-js: dependencies: @@ -1451,7 +1451,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) yargs: specifier: 'catalog:' version: 18.0.0 @@ -1555,7 +1555,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/json-schema: dependencies: @@ -1601,7 +1601,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/library-linter: devDependencies: @@ -1625,7 +1625,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/monarch: dependencies: @@ -1644,7 +1644,7 @@ importers: version: 4.1.3(vitest@4.1.3) happy-dom: specifier: 'catalog:' - version: 20.8.9 + version: 20.9.0 rimraf: specifier: 'catalog:' version: 6.1.3 @@ -1653,7 +1653,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/mutator-framework: devDependencies: @@ -1704,7 +1704,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/openapi3: dependencies: @@ -1783,7 +1783,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/pack: dependencies: @@ -1817,7 +1817,7 @@ importers: version: 8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/playground: dependencies: @@ -1956,7 +1956,7 @@ importers: version: 4.5.4(@types/node@25.5.2)(rollup@4.60.1)(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/playground-website: dependencies: @@ -2071,7 +2071,7 @@ importers: version: 4.5.4(@types/node@25.5.2)(rollup@4.60.1)(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/prettier-plugin-typespec: dependencies: @@ -2090,7 +2090,7 @@ importers: version: 0.28.0 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/protobuf: devDependencies: @@ -2123,7 +2123,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/react-components: dependencies: @@ -2187,7 +2187,7 @@ importers: version: 4.5.4(@types/node@25.5.2)(rollup@4.60.1)(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/rest: devDependencies: @@ -2220,7 +2220,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/samples: dependencies: @@ -2293,7 +2293,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/spec: devDependencies: @@ -2314,7 +2314,7 @@ importers: version: 5.2.1 fast-xml-parser: specifier: 'catalog:' - version: 5.5.10 + version: 5.7.1 devDependencies: '@types/express': specifier: 'catalog:' @@ -2339,7 +2339,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/spec-coverage-sdk: dependencies: @@ -2531,7 +2531,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/standalone: dependencies: @@ -2583,7 +2583,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/streams: devDependencies: @@ -2613,7 +2613,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/tmlanguage-generator: dependencies: @@ -2672,7 +2672,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/tspd: dependencies: @@ -2751,7 +2751,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/typespec-vs: devDependencies: @@ -2829,7 +2829,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vscode-languageclient: specifier: 'catalog:' version: 9.0.1 @@ -2868,7 +2868,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/xml: devDependencies: @@ -2898,7 +2898,7 @@ importers: version: 6.0.2 vitest: specifier: 'catalog:' - version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) website: dependencies: @@ -2910,7 +2910,7 @@ importers: version: 5.0.3(@types/node@25.5.2)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) '@astrojs/starlight': specifier: 'catalog:' - version: 0.38.3(astro@6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + version: 0.38.3(astro@6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) '@docsearch/css': specifier: 'catalog:' version: 4.6.2 @@ -2934,10 +2934,10 @@ importers: version: link:../packages/playground astro: specifier: 'catalog:' - version: 6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) astro-rehype-relative-markdown-links: specifier: 'catalog:' - version: 0.19.0(astro@6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + version: 0.19.0(astro@6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) clsx: specifier: 'catalog:' version: 2.1.1 @@ -3031,7 +3031,7 @@ importers: version: link:../packages/xml astro-expressive-code: specifier: 'catalog:' - version: 0.41.7(astro@6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + version: 0.41.7(astro@6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) rehype-mermaid: specifier: 'catalog:' version: 3.0.0(playwright@1.59.1) @@ -3198,8 +3198,8 @@ packages: peerDependencies: astro: ^6.0.0 - '@astrojs/telemetry@3.3.0': - resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==} + '@astrojs/telemetry@3.3.1': + resolution: {integrity: sha512-7fcIxXS9J4ls5tr8b3ww9rbAIz2+HrhNJYZdkAhhB4za/I5IZ/60g+Bs8q7zwG0tOIZfNB4JWhVJ1Qkl/OrNCw==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} '@astrojs/yaml2ts@0.2.3': @@ -5234,6 +5234,9 @@ packages: '@nevware21/ts-utils@0.13.0': resolution: {integrity: sha512-F3mD+DsUn9OiZmZc5tg0oKqrJCtiCstwx+wE+DNzFYh2cCRUuzTYdK9zGGP/au2BWvbOQ6Tqlbjr2+dT1P3AlQ==} + '@nodable/entities@2.1.0': + resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -7009,6 +7012,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.58.2': + resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/rule-tester@8.58.1': resolution: {integrity: sha512-xNpISfU2bSCaw4zOy81xZJ3zC+CV6byOGRtMJGheAVqLGhRCX6NcA1UcKMpIWu4Vva8Jh76+j6VoeKKtYbeXNQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -7019,12 +7028,22 @@ packages: resolution: {integrity: sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.58.2': + resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.58.1': resolution: {integrity: sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/tsconfig-utils@8.58.2': + resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/type-utils@8.58.1': resolution: {integrity: sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -7036,12 +7055,22 @@ packages: resolution: {integrity: sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.58.2': + resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.58.1': resolution: {integrity: sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/typescript-estree@8.58.2': + resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.58.1': resolution: {integrity: sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -7049,10 +7078,21 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.58.2': + resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/visitor-keys@8.58.1': resolution: {integrity: sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.58.2': + resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typespec/ts-http-runtime@0.3.4': resolution: {integrity: sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==} engines: {node: '>=20.0.0'} @@ -7091,8 +7131,8 @@ packages: '@vitest/browser': optional: true - '@vitest/eslint-plugin@1.6.14': - resolution: {integrity: sha512-PXZ5ysw4eHU9h8nDtBvVcGC7Z2C/T9CFdheqSw1NNXFYqViojub0V9bgdYI67iBTOcra2mwD0EYldlY9bGPf2Q==} + '@vitest/eslint-plugin@1.6.16': + resolution: {integrity: sha512-2pBN1F1JXq6zTSaYC58CMJa7pGxXIRsLfOioeZM4cPE3pRdSh1ySTSoHPQlOTEF5WgoVzWZQxhGQ3ygT78hOVg==} engines: {node: '>=18'} peerDependencies: '@typescript-eslint/eslint-plugin': '*' @@ -7345,19 +7385,19 @@ packages: '@yarnpkg/cli': ^4.9.2 '@yarnpkg/core': ^4.4.2 - '@yarnpkg/plugin-essentials@4.4.5': - resolution: {integrity: sha512-vRQ7UoMK0okr+IVXkLu6JrpANYvmsZ8ljGQhNDI6XhOIU0pSPWD3I0JRh1449SMdQQtn8q8n0uwrzzUcvmQdFA==} + '@yarnpkg/plugin-essentials@4.5.0': + resolution: {integrity: sha512-LGQYbgPpeorHAJtscrGOLdNCaSyqee3wo4NnkfJmeGWvlPZyG6eRguthY1AXixo2ZxdN/YkeoI/GAhZH5bRvZA==} engines: {node: '>=18.12.0'} peerDependencies: - '@yarnpkg/cli': ^4.13.0 - '@yarnpkg/core': ^4.6.0 - '@yarnpkg/plugin-git': ^3.1.4 + '@yarnpkg/cli': ^4.14.0 + '@yarnpkg/core': ^4.7.0 + '@yarnpkg/plugin-git': ^3.2.0 - '@yarnpkg/plugin-exec@3.0.2': - resolution: {integrity: sha512-lvBq0tc/k4CyvxFEhohiw/5rUi0kRWuFnAUnoXn6c0GJaldfK8gCyYhKfxwFZmcDOfM8h2JrDtzZdWqVk5D7lw==} + '@yarnpkg/plugin-exec@3.1.0': + resolution: {integrity: sha512-c0PInm8BbyNwGorVJPZyvt2L03OsccLdtmuwtl4sCxSYQjrJ3fwKVhOstL9KaxpUBQ+I9tVzoJzzpe8SzySZ0A==} engines: {node: '>=18.12.0'} peerDependencies: - '@yarnpkg/core': ^4.4.2 + '@yarnpkg/core': ^4.7.0 '@yarnpkg/plugin-file@3.0.2': resolution: {integrity: sha512-sL47+nbBs5yC2MQS8ihKm1PzeVLPuZihWQRw0UCu1+2H5qgHV8hA/4kCvMSx98amksq4UjP8ybeBFrRvsdhAHA==} @@ -7365,11 +7405,11 @@ packages: peerDependencies: '@yarnpkg/core': ^4.4.2 - '@yarnpkg/plugin-git@3.1.4': - resolution: {integrity: sha512-kopHiWTx4ANTk32i+rG/45oUtpK0l/MklQ2ZmIINete+4ZcTvB0cVr4vOw1+0owJ+s4iZMMpeu4RO/wz19BeHQ==} + '@yarnpkg/plugin-git@3.2.0': + resolution: {integrity: sha512-i/+3fJ7UYqAwmnfKYEc6Yrs9Y2mDVeCIWGXSpyQSZXokDYG0+YSf0ZSqlij4sFs5zSEOCkY0pK3wj6d4NcvhkQ==} engines: {node: '>=18.12.0'} peerDependencies: - '@yarnpkg/core': ^4.5.0 + '@yarnpkg/core': ^4.7.0 '@yarnpkg/plugin-github@3.0.2': resolution: {integrity: sha512-NHEaxJkzBC59Z97I30fleJlm6jE7CVY7cXaDD+kYwzIp/qKCb7IaJBp3MqUhCRyvyerNYRf08nIO+PXJ9odMtg==} @@ -7434,6 +7474,13 @@ packages: '@yarnpkg/core': ^4.6.0 '@yarnpkg/plugin-pack': ^4.0.4 + '@yarnpkg/plugin-npm@3.5.0': + resolution: {integrity: sha512-Bs0timHs48gAu8IBCHsevRq8BqZNAnNCXgdnHEeKJE97H9gds0+tY7BsBzSYCtCdVVmsDB+l0Ia38QZAG8RMMw==} + engines: {node: '>=18.12.0'} + peerDependencies: + '@yarnpkg/core': ^4.7.0 + '@yarnpkg/plugin-pack': ^4.0.4 + '@yarnpkg/plugin-pack@4.0.4': resolution: {integrity: sha512-P+lLCMUsvAr8AXWzrgPYqUtZsBl7nhv7zM/x6jV06czyEcApRKWWJw42ekiFa6+xBlwU4ddvHZK4eWKYf27TIw==} engines: {node: '>=18.12.0'} @@ -7455,6 +7502,13 @@ packages: '@yarnpkg/cli': ^4.11.0 '@yarnpkg/core': ^4.5.0 + '@yarnpkg/plugin-pnp@4.1.5': + resolution: {integrity: sha512-KdrC2/kcr2WSD7yehBQPXCVIZSk7Cf3V6TUxnlUrdqC4T65XrJH423f77GtUJnsG36BE0aPYDUKIcIliPBcq6g==} + engines: {node: '>=18.12.0'} + peerDependencies: + '@yarnpkg/cli': ^4.14.1 + '@yarnpkg/core': ^4.7.0 + '@yarnpkg/plugin-pnpm@2.1.2': resolution: {integrity: sha512-6lR84Gu0LnS2MUKu3dFxyGTXmHbODVghHH0x9OyCsdMwWQp2GHJIsuda1dF+lbJdVZJleGbAUSchnqIFx8QY9A==} engines: {node: '>=18.12.0'} @@ -7497,6 +7551,10 @@ packages: resolution: {integrity: sha512-PsRujup+6DpgXexQe0Wh4h+syQhdruhounJjqbBMXV3meOzqr7k0Nj9+jwQ4t16EZJrhVxH7vRvjZ+VitH5aWQ==} engines: {node: '>=18.12.0'} + '@yarnpkg/pnp@4.1.5': + resolution: {integrity: sha512-hDqB3Kke873wdx3rip1zlblfBIx2sGJH0BoNPh435A5rSEVT1sDKuCykLxNlLUnGhIZc83FXLGDGZJn23ggAQg==} + engines: {node: '>=18.12.0'} + '@yarnpkg/shell@4.0.0': resolution: {integrity: sha512-Yk2gyiQvsoee/jXP9q0jMl412Nx27LYu+P1O4DHuxeutL9qtd6t3Ktuv+zZmOzFc6gMQ7+/6mQFPo3/LlXZM3w==} engines: {node: '>=18.12.0'} @@ -7734,8 +7792,8 @@ packages: peerDependencies: astro: '>=2 <7' - astro@6.1.5: - resolution: {integrity: sha512-AJVw/JlssxUCBFi3Hp4djL8Pt7wUQqStBBawCd8cNGBBM2lBzp/rXGguzt4OcMfW+86fs0hpFwMyopHM2r6d3g==} + astro@6.1.8: + resolution: {integrity: sha512-6fT9M12U3fpi13DiPavNKDIoBflASTSxmKTEe+zXhWtlebQuOqfOnIrMWyRmlXp+mgDsojmw+fVFG9LUTzKSog==} engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true @@ -8724,10 +8782,6 @@ packages: resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==} engines: {node: '>=0.3.1'} - diff@8.0.3: - resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} - engines: {node: '>=0.3.1'} - diff@8.0.4: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} @@ -9159,10 +9213,17 @@ packages: fast-xml-builder@1.1.4: resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + fast-xml-builder@1.1.5: + resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==} + fast-xml-parser@5.5.10: resolution: {integrity: sha512-go2J2xODMc32hT+4Xr/bBGXMaIoiCwrwp2mMtAvKyvEFW6S/v5Gn2pBmE4nvbwNjGhpcAiOwEv7R6/GZ6XRa9w==} hasBin: true + fast-xml-parser@5.7.1: + resolution: {integrity: sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA==} + hasBin: true + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -9461,8 +9522,8 @@ packages: hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} - happy-dom@20.8.9: - resolution: {integrity: sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA==} + happy-dom@20.9.0: + resolution: {integrity: sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==} engines: {node: '>=20.0.0'} has-flag@3.0.0: @@ -9813,6 +9874,11 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-docker@4.0.0: + resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==} + engines: {node: '>=20'} + hasBin: true + is-extendable@0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} @@ -10996,9 +11062,6 @@ packages: oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} - oniguruma-to-es@4.3.4: - resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} - oniguruma-to-es@4.3.5: resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==} @@ -11208,6 +11271,10 @@ packages: resolution: {integrity: sha512-s4DQMxIdhj3jLFWd9LxHOplj4p9yQ4ffMGowFf3cpEgrrJjEhN0V5nxw4Ye1EViAGDoL4/1AeO6qHpqYPOzE4Q==} engines: {node: '>=14.0.0'} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -13423,18 +13490,6 @@ packages: utf-8-validate: optional: true - ws@8.19.0: - resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.20.0: resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} @@ -13819,12 +13874,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/mdx@5.0.3(astro@6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': + '@astrojs/mdx@5.0.3(astro@6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': dependencies: '@astrojs/markdown-remark': 7.1.0 '@mdx-js/mdx': 3.1.1 acorn: 8.16.0 - astro: 6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + astro: 6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) es-module-lexer: 2.0.0 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -13873,17 +13928,17 @@ snapshots: stream-replace-string: 2.0.0 zod: 4.3.6 - '@astrojs/starlight@0.38.3(astro@6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': + '@astrojs/starlight@0.38.3(astro@6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': dependencies: '@astrojs/markdown-remark': 7.1.0 - '@astrojs/mdx': 5.0.3(astro@6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + '@astrojs/mdx': 5.0.3(astro@6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) '@astrojs/sitemap': 3.7.2 '@pagefind/default-ui': 1.5.0 '@types/hast': 3.0.4 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 - astro: 6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) - astro-expressive-code: 0.41.7(astro@6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + astro: 6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + astro-expressive-code: 0.41.7(astro@6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) bcp-47: 2.1.0 hast-util-from-html: 2.0.3 hast-util-select: 6.0.4 @@ -13907,17 +13962,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/telemetry@3.3.0': + '@astrojs/telemetry@3.3.1': dependencies: ci-info: 4.4.0 - debug: 4.4.3 dlv: 1.1.3 dset: 3.1.4 - is-docker: 3.0.0 + is-docker: 4.0.0 is-wsl: 3.1.1 which-pm-runs: 1.1.0 - transitivePeerDependencies: - - supports-color '@astrojs/yaml2ts@0.2.3': dependencies: @@ -16548,7 +16600,7 @@ snapshots: '@rushstack/rig-package': 0.7.2 '@rushstack/terminal': 0.22.3(@types/node@25.5.2) '@rushstack/ts-command-line': 5.3.3(@types/node@25.5.2) - diff: 8.0.3 + diff: 8.0.4 lodash: 4.17.23 minimatch: 10.2.3 resolve: 1.22.11 @@ -16644,6 +16696,8 @@ snapshots: '@nevware21/ts-utils@0.13.0': {} + '@nodable/entities@2.1.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -18305,7 +18359,7 @@ snapshots: dependencies: '@shikijs/types': 3.23.0 '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.4 + oniguruma-to-es: 4.3.5 '@shikijs/engine-javascript@4.0.2': dependencies: @@ -19019,6 +19073,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.58.2(typescript@6.0.2)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2) + '@typescript-eslint/types': 8.58.2 + debug: 4.4.3 + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/rule-tester@8.58.1(eslint@10.2.0)(typescript@6.0.2)': dependencies: '@typescript-eslint/parser': 8.58.1(eslint@10.2.0)(typescript@6.0.2) @@ -19038,10 +19101,19 @@ snapshots: '@typescript-eslint/types': 8.58.1 '@typescript-eslint/visitor-keys': 8.58.1 + '@typescript-eslint/scope-manager@8.58.2': + dependencies: + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/tsconfig-utils@8.58.1(typescript@6.0.2)': dependencies: typescript: 6.0.2 + '@typescript-eslint/tsconfig-utils@8.58.2(typescript@6.0.2)': + dependencies: + typescript: 6.0.2 + '@typescript-eslint/type-utils@8.58.1(eslint@10.2.0)(typescript@6.0.2)': dependencies: '@typescript-eslint/types': 8.58.1 @@ -19056,6 +19128,8 @@ snapshots: '@typescript-eslint/types@8.58.1': {} + '@typescript-eslint/types@8.58.2': {} + '@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.2)': dependencies: '@typescript-eslint/project-service': 8.58.1(typescript@6.0.2) @@ -19071,6 +19145,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2)': + dependencies: + '@typescript-eslint/project-service': 8.58.2(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2) + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/visitor-keys': 8.58.2 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.58.1(eslint@10.2.0)(typescript@6.0.2)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) @@ -19082,11 +19171,27 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.58.2(eslint@10.2.0)(typescript@6.0.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) + eslint: 10.2.0 + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.58.1': dependencies: '@typescript-eslint/types': 8.58.1 eslint-visitor-keys: 5.0.1 + '@typescript-eslint/visitor-keys@8.58.2': + dependencies: + '@typescript-eslint/types': 8.58.2 + eslint-visitor-keys: 5.0.1 + '@typespec/ts-http-runtime@0.3.4': dependencies: http-proxy-agent: 7.0.2 @@ -19131,17 +19236,17 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/eslint-plugin@1.6.14(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2)(vitest@4.1.3)': + '@vitest/eslint-plugin@1.6.16(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2)(vitest@4.1.3)': dependencies: - '@typescript-eslint/scope-manager': 8.58.1 - '@typescript-eslint/utils': 8.58.1(eslint@10.2.0)(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/utils': 8.58.2(eslint@10.2.0)(typescript@6.0.2) eslint: 10.2.0 optionalDependencies: '@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2) typescript: 6.0.2 - vitest: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - supports-color @@ -19205,7 +19310,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vitest: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/utils@3.2.4': dependencies: @@ -19436,27 +19541,27 @@ snapshots: '@yarnpkg/plugin-compat': 4.0.12(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-patch@4.0.3(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) '@yarnpkg/plugin-constraints': 4.0.5(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) '@yarnpkg/plugin-dlx': 4.0.2(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) - '@yarnpkg/plugin-essentials': 4.4.5(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) - '@yarnpkg/plugin-exec': 3.0.2(@yarnpkg/core@4.6.0(typanion@3.14.0)) + '@yarnpkg/plugin-essentials': 4.5.0(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) + '@yarnpkg/plugin-exec': 3.1.0(@yarnpkg/core@4.6.0(typanion@3.14.0)) '@yarnpkg/plugin-file': 3.0.2(@yarnpkg/core@4.6.0(typanion@3.14.0)) - '@yarnpkg/plugin-git': 3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) - '@yarnpkg/plugin-github': 3.0.2(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) + '@yarnpkg/plugin-git': 3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) + '@yarnpkg/plugin-github': 3.0.2(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) '@yarnpkg/plugin-http': 3.0.3(@yarnpkg/core@4.6.0(typanion@3.14.0)) '@yarnpkg/plugin-init': 4.1.2(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) - '@yarnpkg/plugin-interactive-tools': 4.1.0(@types/react@19.2.14)(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-essentials@4.4.5(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0))) + '@yarnpkg/plugin-interactive-tools': 4.1.0(@types/react@19.2.14)(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-essentials@4.5.0(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0))) '@yarnpkg/plugin-jsr': 1.1.1(@yarnpkg/core@4.6.0(typanion@3.14.0)) '@yarnpkg/plugin-link': 3.0.2(@yarnpkg/core@4.6.0(typanion@3.14.0)) '@yarnpkg/plugin-nm': 4.0.8(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) - '@yarnpkg/plugin-npm': 3.4.1(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-pack@4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) - '@yarnpkg/plugin-npm-cli': 4.4.1(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-npm@3.4.1(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-pack@4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)))(@yarnpkg/plugin-pack@4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) + '@yarnpkg/plugin-npm': 3.5.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-pack@4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) + '@yarnpkg/plugin-npm-cli': 4.4.1(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-npm@3.5.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-pack@4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)))(@yarnpkg/plugin-pack@4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) '@yarnpkg/plugin-pack': 4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) '@yarnpkg/plugin-patch': 4.0.3(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) - '@yarnpkg/plugin-pnp': 4.1.3(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) + '@yarnpkg/plugin-pnp': 4.1.5(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) '@yarnpkg/plugin-pnpm': 2.1.2(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) '@yarnpkg/plugin-stage': 4.0.2(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) - '@yarnpkg/plugin-typescript': 4.1.3(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-essentials@4.4.5(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)))(typanion@3.14.0) - '@yarnpkg/plugin-version': 4.2.0(@types/react@19.2.14)(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0))(typanion@3.14.0) - '@yarnpkg/plugin-workspace-tools': 4.1.7(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) + '@yarnpkg/plugin-typescript': 4.1.3(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-essentials@4.5.0(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)))(typanion@3.14.0) + '@yarnpkg/plugin-version': 4.2.0(@types/react@19.2.14)(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0))(typanion@3.14.0) + '@yarnpkg/plugin-workspace-tools': 4.1.7(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) '@yarnpkg/shell': 4.1.3(typanion@3.14.0) ci-info: 4.4.0 clipanion: 4.0.0-rc.4(typanion@3.14.0) @@ -19568,13 +19673,13 @@ snapshots: transitivePeerDependencies: - typanion - '@yarnpkg/plugin-essentials@4.4.5(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0))': + '@yarnpkg/plugin-essentials@4.5.0(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0))': dependencies: '@yarnpkg/cli': 4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)) '@yarnpkg/core': 4.6.0(typanion@3.14.0) '@yarnpkg/fslib': 3.1.5 '@yarnpkg/parsers': 3.0.3 - '@yarnpkg/plugin-git': 3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) + '@yarnpkg/plugin-git': 3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) ci-info: 4.4.0 clipanion: 4.0.0-rc.4(typanion@3.14.0) enquirer: 2.4.1 @@ -19584,7 +19689,7 @@ snapshots: tslib: 2.8.1 typanion: 3.14.0 - '@yarnpkg/plugin-exec@3.0.2(@yarnpkg/core@4.6.0(typanion@3.14.0))': + '@yarnpkg/plugin-exec@3.1.0(@yarnpkg/core@4.6.0(typanion@3.14.0))': dependencies: '@yarnpkg/core': 4.6.0(typanion@3.14.0) '@yarnpkg/fslib': 3.1.5 @@ -19597,7 +19702,7 @@ snapshots: '@yarnpkg/libzip': 3.2.2(@yarnpkg/fslib@3.1.5) tslib: 2.8.1 - '@yarnpkg/plugin-git@3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)': + '@yarnpkg/plugin-git@3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)': dependencies: '@types/semver': 7.7.1 '@yarnpkg/core': 4.6.0(typanion@3.14.0) @@ -19610,11 +19715,11 @@ snapshots: transitivePeerDependencies: - typanion - '@yarnpkg/plugin-github@3.0.2(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0))': + '@yarnpkg/plugin-github@3.0.2(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0))': dependencies: '@yarnpkg/core': 4.6.0(typanion@3.14.0) '@yarnpkg/fslib': 3.1.5 - '@yarnpkg/plugin-git': 3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) + '@yarnpkg/plugin-git': 3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) tslib: 2.8.1 '@yarnpkg/plugin-http@3.0.3(@yarnpkg/core@4.6.0(typanion@3.14.0))': @@ -19632,12 +19737,12 @@ snapshots: transitivePeerDependencies: - typanion - '@yarnpkg/plugin-interactive-tools@4.1.0(@types/react@19.2.14)(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-essentials@4.4.5(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)))': + '@yarnpkg/plugin-interactive-tools@4.1.0(@types/react@19.2.14)(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-essentials@4.5.0(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)))': dependencies: '@yarnpkg/cli': 4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)) '@yarnpkg/core': 4.6.0(typanion@3.14.0) '@yarnpkg/libui': 3.1.0(ink@3.2.0(@types/react@19.2.14)(react@17.0.2))(react@17.0.2) - '@yarnpkg/plugin-essentials': 4.4.5(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) + '@yarnpkg/plugin-essentials': 4.5.0(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) algoliasearch: 4.27.0 clipanion: 4.0.0-rc.4(typanion@3.14.0) diff: 5.2.2 @@ -19680,12 +19785,12 @@ snapshots: transitivePeerDependencies: - typanion - '@yarnpkg/plugin-npm-cli@4.4.1(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-npm@3.4.1(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-pack@4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)))(@yarnpkg/plugin-pack@4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0))': + '@yarnpkg/plugin-npm-cli@4.4.1(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-npm@3.5.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-pack@4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)))(@yarnpkg/plugin-pack@4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0))': dependencies: '@yarnpkg/cli': 4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)) '@yarnpkg/core': 4.6.0(typanion@3.14.0) '@yarnpkg/fslib': 3.1.5 - '@yarnpkg/plugin-npm': 3.4.1(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-pack@4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) + '@yarnpkg/plugin-npm': 3.5.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-pack@4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) '@yarnpkg/plugin-pack': 4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) clipanion: 4.0.0-rc.4(typanion@3.14.0) enquirer: 2.4.1 @@ -19709,6 +19814,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@yarnpkg/plugin-npm@3.5.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-pack@4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0))': + dependencies: + '@yarnpkg/core': 4.6.0(typanion@3.14.0) + '@yarnpkg/fslib': 3.1.5 + '@yarnpkg/plugin-pack': 4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) + enquirer: 2.4.1 + es-toolkit: 1.45.1 + micromatch: 4.0.8 + semver: 7.7.4 + sigstore: 3.1.0 + ssri: 12.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@yarnpkg/plugin-pack@4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)': dependencies: '@yarnpkg/cli': 4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)) @@ -19745,12 +19865,25 @@ snapshots: transitivePeerDependencies: - typanion + '@yarnpkg/plugin-pnp@4.1.5(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)': + dependencies: + '@yarnpkg/cli': 4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)) + '@yarnpkg/core': 4.6.0(typanion@3.14.0) + '@yarnpkg/fslib': 3.1.5 + '@yarnpkg/plugin-stage': 4.0.2(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) + '@yarnpkg/pnp': 4.1.5 + clipanion: 4.0.0-rc.4(typanion@3.14.0) + micromatch: 4.0.8 + tslib: 2.8.1 + transitivePeerDependencies: + - typanion + '@yarnpkg/plugin-pnpm@2.1.2(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)': dependencies: '@yarnpkg/cli': 4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)) '@yarnpkg/core': 4.6.0(typanion@3.14.0) '@yarnpkg/fslib': 3.1.5 - '@yarnpkg/plugin-pnp': 4.1.3(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) + '@yarnpkg/plugin-pnp': 4.1.5(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) '@yarnpkg/plugin-stage': 4.0.2(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) clipanion: 4.0.0-rc.4(typanion@3.14.0) p-limit: 2.3.0 @@ -19768,12 +19901,12 @@ snapshots: transitivePeerDependencies: - typanion - '@yarnpkg/plugin-typescript@4.1.3(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-essentials@4.4.5(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)))(typanion@3.14.0)': + '@yarnpkg/plugin-typescript@4.1.3(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-essentials@4.5.0(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)))(typanion@3.14.0)': dependencies: '@yarnpkg/cli': 4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)) '@yarnpkg/core': 4.6.0(typanion@3.14.0) '@yarnpkg/fslib': 3.1.5 - '@yarnpkg/plugin-essentials': 4.4.5(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) + '@yarnpkg/plugin-essentials': 4.5.0(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0)) '@yarnpkg/plugin-pack': 4.0.4(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) algoliasearch: 4.27.0 semver: 7.7.4 @@ -19781,14 +19914,14 @@ snapshots: transitivePeerDependencies: - typanion - '@yarnpkg/plugin-version@4.2.0(@types/react@19.2.14)(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0))(typanion@3.14.0)': + '@yarnpkg/plugin-version@4.2.0(@types/react@19.2.14)(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0))(typanion@3.14.0)': dependencies: '@yarnpkg/cli': 4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)) '@yarnpkg/core': 4.6.0(typanion@3.14.0) '@yarnpkg/fslib': 3.1.5 '@yarnpkg/libui': 3.1.0(ink@3.2.0(@types/react@19.2.14)(react@17.0.2))(react@17.0.2) '@yarnpkg/parsers': 3.0.3 - '@yarnpkg/plugin-git': 3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) + '@yarnpkg/plugin-git': 3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) clipanion: 4.0.0-rc.4(typanion@3.14.0) es-toolkit: 1.45.1 ink: 3.2.0(@types/react@19.2.14)(react@17.0.2) @@ -19801,12 +19934,12 @@ snapshots: - typanion - utf-8-validate - '@yarnpkg/plugin-workspace-tools@4.1.7(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0))': + '@yarnpkg/plugin-workspace-tools@4.1.7(@yarnpkg/cli@4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)))(@yarnpkg/core@4.6.0(typanion@3.14.0))(@yarnpkg/plugin-git@3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0))': dependencies: '@yarnpkg/cli': 4.12.0(@types/react@19.2.14)(@yarnpkg/core@4.6.0(typanion@3.14.0)) '@yarnpkg/core': 4.6.0(typanion@3.14.0) '@yarnpkg/fslib': 3.1.5 - '@yarnpkg/plugin-git': 3.1.4(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) + '@yarnpkg/plugin-git': 3.2.0(@yarnpkg/core@4.6.0(typanion@3.14.0))(typanion@3.14.0) clipanion: 4.0.0-rc.4(typanion@3.14.0) es-toolkit: 1.45.1 micromatch: 4.0.8 @@ -19819,6 +19952,11 @@ snapshots: '@types/node': 18.19.130 '@yarnpkg/fslib': 3.1.5 + '@yarnpkg/pnp@4.1.5': + dependencies: + '@types/node': 18.19.130 + '@yarnpkg/fslib': 3.1.5 + '@yarnpkg/shell@4.0.0(typanion@3.14.0)': dependencies: '@yarnpkg/fslib': 3.1.5 @@ -20053,14 +20191,14 @@ snapshots: astring@1.9.0: {} - astro-expressive-code@0.41.7(astro@6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)): + astro-expressive-code@0.41.7(astro@6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)): dependencies: - astro: 6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + astro: 6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) rehype-expressive-code: 0.41.7 - astro-rehype-relative-markdown-links@0.19.0(astro@6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)): + astro-rehype-relative-markdown-links@0.19.0(astro@6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)): dependencies: - astro: 6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + astro: 6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) catch-unknown: 2.0.0 debug: 4.4.3 github-slugger: 2.0.0 @@ -20072,12 +20210,12 @@ snapshots: transitivePeerDependencies: - supports-color - astro@6.1.5(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3): + astro@6.1.8(@azure/identity@4.13.1)(@azure/storage-blob@12.31.0)(@types/node@25.5.2)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3): dependencies: '@astrojs/compiler': 3.0.1 '@astrojs/internal-helpers': 0.8.0 '@astrojs/markdown-remark': 7.1.0 - '@astrojs/telemetry': 3.3.0 + '@astrojs/telemetry': 3.3.1 '@capsizecss/unpack': 4.0.0 '@clack/prompts': 1.2.0 '@oslojs/encoding': 1.1.0 @@ -21198,8 +21336,6 @@ snapshots: diff@5.2.2: {} - diff@8.0.3: {} - diff@8.0.4: {} dir-glob@3.0.1: @@ -21768,12 +21904,23 @@ snapshots: dependencies: path-expression-matcher: 1.4.0 + fast-xml-builder@1.1.5: + dependencies: + path-expression-matcher: 1.5.0 + fast-xml-parser@5.5.10: dependencies: fast-xml-builder: 1.1.4 path-expression-matcher: 1.4.0 strnum: 2.2.3 + fast-xml-parser@5.7.1: + dependencies: + '@nodable/entities': 2.1.0 + fast-xml-builder: 1.1.5 + path-expression-matcher: 1.5.0 + strnum: 2.2.3 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -22121,14 +22268,14 @@ snapshots: hachure-fill@0.5.2: {} - happy-dom@20.8.9: + happy-dom@20.9.0: dependencies: '@types/node': 25.5.2 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 7.0.1 whatwg-mimetype: 3.0.0 - ws: 8.19.0 + ws: 8.20.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -22630,6 +22777,8 @@ snapshots: is-docker@3.0.0: {} + is-docker@4.0.0: {} + is-extendable@0.1.1: {} is-extglob@2.1.1: {} @@ -24121,12 +24270,6 @@ snapshots: oniguruma-parser@0.12.1: {} - oniguruma-to-es@4.3.4: - dependencies: - oniguruma-parser: 0.12.1 - regex: 6.1.0 - regex-recursion: 6.0.2 - oniguruma-to-es@4.3.5: dependencies: oniguruma-parser: 0.12.1 @@ -24375,6 +24518,8 @@ snapshots: path-expression-matcher@1.4.0: {} + path-expression-matcher@1.5.0: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -26443,7 +26588,7 @@ snapshots: optionalDependencies: vite: 7.3.2(@types/node@25.5.2)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vitest@4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.9.0)(jsdom@25.0.1)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.3 '@vitest/mocker': 4.1.3(vite@8.0.8(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -26469,7 +26614,7 @@ snapshots: '@types/node': 25.5.2 '@vitest/coverage-v8': 4.1.3(vitest@4.1.3) '@vitest/ui': 4.1.3(vitest@4.1.3) - happy-dom: 20.8.9 + happy-dom: 20.9.0 jsdom: 25.0.1 transitivePeerDependencies: - msw @@ -26693,8 +26838,6 @@ snapshots: ws@7.5.10: {} - ws@8.19.0: {} - ws@8.20.0: {} wsl-utils@0.1.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9b03161a0c4..e258e62e313 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -75,7 +75,7 @@ catalog: "@typespec/ts-http-runtime": 0.3.4 "@vitejs/plugin-react": ^6.0.1 "@vitest/coverage-v8": ^4.1.3 - "@vitest/eslint-plugin": ^1.6.14 + "@vitest/eslint-plugin": ^1.6.16 "@vitest/ui": ^4.1.3 "@vscode/extension-telemetry": ^1.5.1 "@vscode/test-electron": ^2.5.2 @@ -88,7 +88,7 @@ catalog: "@yarnpkg/plugin-pnp": ^4.1.3 ajv: ^8.18.0 ajv-formats: ^3.0.1 - astro: ^6.1.5 + astro: ^6.1.8 astro-expressive-code: ^0.41.7 astro-rehype-relative-markdown-links: ^0.19.0 c8: ^11.0.0 @@ -111,8 +111,8 @@ catalog: eslint-plugin-unicorn: ^64.0.0 execa: ^9.6.1 express: ^5.2.1 - fast-xml-parser: ^5.5.9 - happy-dom: ^20.8.9 + fast-xml-parser: ^5.7.0 + happy-dom: ^20.9.0 is-unicode-supported: ^2.1.0 log-symbols: ^7.0.1 lzutf8: 0.6.3 diff --git a/website/src/components/playground-component/playground.tsx b/website/src/components/playground-component/playground.tsx index 6544c1ac7c1..dc55e0f64cc 100644 --- a/website/src/components/playground-component/playground.tsx +++ b/website/src/components/playground-component/playground.tsx @@ -37,8 +37,8 @@ export const WebsitePlayground = ({ versionData }: WebsitePlaygroundProps) => { libraries={imports} emitterViewers={{ "@typespec/openapi3": [SwaggerUIViewer] }} emitterOptions={{ - "@typespec/http-client-python": { debounce: 500, newChangeDiff: true }, - "@typespec/http-client-csharp": { debounce: 500, newChangeDiff: true }, + "@typespec/http-client-python": { debounce: 500 }, + "@typespec/http-client-csharp": { debounce: 500 }, }} importConfig={{ useShim: true }} editorOptions={editorOptions} diff --git a/website/src/components/react-pages/playground.tsx b/website/src/components/react-pages/playground.tsx index 5ed019fabbb..326124bdf55 100644 --- a/website/src/components/react-pages/playground.tsx +++ b/website/src/components/react-pages/playground.tsx @@ -3,7 +3,10 @@ import { useEffect, useState, type ReactNode } from "react"; import { FluentLayout } from "../fluent/fluent-layout"; import { loadImportMap, type VersionData } from "../playground-component/import-map"; -const additionalPlaygroundPackages = ["@typespec/http-client-python"]; +const additionalPlaygroundPackages = [ + "@typespec/http-client-csharp", + "@typespec/http-client-python", +]; export const AsyncPlayground = ({ latestVersion, diff --git a/website/src/content/docs/release-notes/typespec-1-11-0.md b/website/src/content/docs/release-notes/typespec-1-11-0.md deleted file mode 100644 index b082dc5d50e..00000000000 --- a/website/src/content/docs/release-notes/typespec-1-11-0.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -slug: release-notes/typespec-1-11-0 -title: "1.11.0" -releaseDate: 2026-04-07 -version: "1.11.0" ---- - -# 1.11.0 - -## Features - -### @typespec/compiler - -- [#9893](https://github.com/microsoft/typespec/pull/9893) Added a new template `FilterVisibility` to support more accurate visibility transforms. This replaces the `@withVisibilityFilter` decorator, which is now deprecated and slated for removal in a future version of TypeSpec. - -## Bug Fixes - -### @typespec/compiler - -- [#10196](https://github.com/microsoft/typespec/pull/10196) Include model name in `duplicate-property` error message -- [#10199](https://github.com/microsoft/typespec/pull/10199) `duplicateDefaultVariant` diagnostic now includes the union type name -- [#10183](https://github.com/microsoft/typespec/pull/10183) Do not interpolate non primitive values in config automatically. - ```yaml - file-type: ["json", "yaml"] - output-file: "openapi.{file-type}" - ``` - Will not be interpolated as `openapi.json,yaml` but keep the placeholder `{file-type}` intact for the emitter to handle. -- [#9893](https://github.com/microsoft/typespec/pull/9893) Fixed a bug that would prevent template parameters from assigning to values in some cases. - -### @typespec/openapi3 - -- [#10041](https://github.com/microsoft/typespec/pull/10041) [importer] Fix `anyOf` with `$ref` and inline object being incorrectly imported as a model instead of a union. -- [#10046](https://github.com/microsoft/typespec/pull/10046) Fix OpenAPI emitter failing with "Duplicate type name" error when using a named union with a `bytes` variant in a multipart body (e.g. `HttpPart` where `MyUnion` includes `bytes`). diff --git a/website/src/content/docs/release-notes/typespec-1-11-0.mdx b/website/src/content/docs/release-notes/typespec-1-11-0.mdx new file mode 100644 index 00000000000..23ed7633339 --- /dev/null +++ b/website/src/content/docs/release-notes/typespec-1-11-0.mdx @@ -0,0 +1,210 @@ +--- +slug: release-notes/typespec-1-11-0 +title: "1.11.0" +releaseDate: 2026-04-07 +version: "1.11.0" +--- + +TypeSpec 1.11.0 improves the authoring loop across modeling, testing, and reviewing generated output. This release introduces `FilterVisibility`, makes Spector scenario matching more semantic, improves Playground navigation, and fixes several compiler, OpenAPI, and generated-server edge cases that previously required workarounds. + +**Thank you to everyone who contributed feedback and fixes for version 1.11**. + +## Highlights + +### `FilterVisibility` is the new path for visibility transforms + +Library authors now have a new `FilterVisibility` template for producing visibility-filtered shapes with better metadata preservation. It also provides the forward-looking replacement path as `@withVisibilityFilter` is deprecated. + +Prefer `FilterVisibility` for new visibility-transform logic, especially when downstream tooling depends on metadata preserved on the transformed shape. + +**Example** + +```typespec +model Example { + @visibility(Lifecycle.Create) + id: string; + + @visibility(Lifecycle.Create, Lifecycle.Read) + name: string; + + @visibility(Lifecycle.Update) + description: string; +} + +model CreateAndReadExample + is FilterVisibility< + Example, + #{ all: #[Lifecycle.Create, Lifecycle.Read] }, + "CreateAndRead{name}" + >; +``` + +:::caution +`@withVisibilityFilter` is now **deprecated**, and `FilterVisibility` provides some guarantees about preserving decorator metadata that were impossible to provide with `@withVisibilityFilter`. We have no plans to remove `@withVisibilityFilter` in any TypeSpec 1.x version, but we recommend that all `@withVisibilityFilter`-based custom visibility templates by migrated to use `FilterVisibility` instead to avoid common issues with the decorator-based visibility filters. +::: + +Related PR: [microsoft/typespec#9893 - [compiler/http] Replace mutative decorators with function calls for visibility and merge-patch](https://github.com/microsoft/typespec/pull/9893) + +### Playground navigation works better on mobile and with larger outputs + +The Playground is easier to use on smaller screens, larger sample collections, and multi-file outputs. You can switch between TypeSpec and output panels on mobile, browse samples by category or search, and navigate generated output in a file tree without adding clutter for single-file results. + +Open the Playground on mobile or with larger sample and output sets to use the new panel switcher, sample search and grouping, and file-tree navigation. + +Related PRs: + +- [microsoft/typespec#10018 - Playground mobile improvements](https://github.com/microsoft/typespec/pull/10018) +- [microsoft/typespec#10024 - TypeSpec Playground: File tree view](https://github.com/microsoft/typespec/pull/10024) +- [microsoft/typespec#10212 - fix: don't show file tree view in playground output for single file](https://github.com/microsoft/typespec/pull/10212) +- [microsoft/typespec#10256 - Add search and category to the gallery](https://github.com/microsoft/typespec/pull/10256) + +### OpenAPI import and emission handle more real-world schemas + +OpenAPI documents that use `anyOf` with both a `$ref` and an inline object are now imported as unions instead of collapsing to a single model. The OpenAPI emitter also no longer fails with duplicate type name errors when a multipart body uses a named union that includes `bytes`. + +This keeps imported TypeSpec closer to the original API design and removes a multipart emission failure that previously required redesigns or workarounds. + +**Example** + +```yaml +components: + schemas: + VoiceIdsOrCustomVoice: + anyOf: + - $ref: "#/components/schemas/VoiceIdsShared" + - type: object + properties: + id: + type: string + required: + - id +``` + +```typespec +union VoiceIdsOrCustomVoice { + VoiceIdsShared, + { + id: string, + }, +} +``` + +Related PRs: + +- [microsoft/typespec#10041 - fix(openapi3): anyOf with $ref + inline object incorrectly imported as model instead of union](https://github.com/microsoft/typespec/pull/10041) +- [microsoft/typespec#10046 - fix(openapi3): resolve "Duplicate type name" error for named union with bytes in multipart body](https://github.com/microsoft/typespec/pull/10046) + +### Spector matchers make scenario tests less brittle + +Spector now includes semantic matchers for values like datetimes, and query/header matching now preserves matcher objects instead of comparing their string representations. That makes cross-language scenario tests less sensitive to formatting differences in timestamps and similar values. + +Use semantic matchers when scenarios need to compare equivalent values rather than exact string formatting. + +**Example** + +```ts +const body = xml`${match.dateTime.rfc3339("2022-08-26T18:38:00.000Z")}`; +``` + +Related PRs: + +- [microsoft/typespec#10011 - Add matcher framework for flexible value comparison in scenarios](https://github.com/microsoft/typespec/pull/10011) +- [microsoft/typespec#10259 - Fix query parameter matcher handling](https://github.com/microsoft/typespec/pull/10259) + +## Additional improvements + +### Emitter config interpolation no longer flattens arrays and objects + +When one config option references another, the compiler now leaves non-primitive values untouched instead of interpolating the default JS string representations of arrays or objects. This makes advanced emitter configuration more predictable when emitters need to resolve structured options themselves. + +Keep placeholders that refer to array- or object-valued options in config and let the emitter resolve them instead of relying on automatic string interpolation. + +```yaml +file-type: ["yaml", "json"] +output-file: "openapi.{file-type}" +``` + +Related PR: [microsoft/typespec#10183 - Do not interpolate non string values automatically](https://github.com/microsoft/typespec/pull/10183) + +### Generated JavaScript servers handle more response and serializer shapes + +Generated JavaScript servers no longer crash when an operation returns a bare scalar or value literal, and enum properties now pass through generated JSON transpose helpers correctly. This makes generated server code more reliable for simple return types and models that require generated JSON serializers. + +Related PRs: + +- [microsoft/typespec#10058 - handle immediate scalar-typed and value-literal typed responses in result processing layer](https://github.com/microsoft/typespec/pull/10058) +- [microsoft/typespec#10059 - handle Enum type in JSON serialization transpose helpers](https://github.com/microsoft/typespec/pull/10059) + +### Duplicate-definition diagnostics are easier to act on + +Duplicate-property and duplicate default-variant errors now identify the model or discriminated union involved, making large specs faster to debug. + +Related PRs: + +- [microsoft/typespec#10196 - include model name in duplicate-property error message](https://github.com/microsoft/typespec/pull/10196) +- [microsoft/typespec#10199 - include union type name in invalid-discriminated-union-variant diagnostic](https://github.com/microsoft/typespec/pull/10199) + +## Full Changelog + +
+ Show all changes + +### `@typespec/compiler` + +#### Features + +- [#9893](https://github.com/microsoft/typespec/pull/9893) Added a new template `FilterVisibility` to support more accurate visibility transforms. This replaces the `@withVisibilityFilter` decorator, which is now deprecated and slated for removal in a future version of TypeSpec. + +#### Bug fixes + +- [#10196](https://github.com/microsoft/typespec/pull/10196) Include model name in `duplicate-property` error message. +- [#10199](https://github.com/microsoft/typespec/pull/10199) `duplicateDefaultVariant` diagnostic now includes the union type name. +- [#10183](https://github.com/microsoft/typespec/pull/10183) Do not interpolate non-primitive values in config automatically. +- [#9893](https://github.com/microsoft/typespec/pull/9893) Fixed a bug that would prevent template parameters from assigning to values in some cases. + +### `@typespec/openapi3` + +#### Bug fixes + +- [#10041](https://github.com/microsoft/typespec/pull/10041) Fix `anyOf` with `$ref` and an inline object being incorrectly imported as a model instead of a union. +- [#10046](https://github.com/microsoft/typespec/pull/10046) Fix OpenAPI emission failing with a duplicate type name error when a named union with `bytes` appears in a multipart body. + +### `@typespec/playground` + +#### Features + +- [#10024](https://github.com/microsoft/typespec/pull/10024) Add a file tree view for output. +- [#10018](https://github.com/microsoft/typespec/pull/10018) Make the UI more mobile friendly, including a panel switcher and a more compact command bar. +- [#10256](https://github.com/microsoft/typespec/pull/10256) Add search and category grouping to samples. + +#### Bug fixes + +- [#10212](https://github.com/microsoft/typespec/pull/10212) Do not show the file tree view in output when there is only a single file. + +### `@typespec/spector` + +#### Features + +- [#10011](https://github.com/microsoft/typespec/pull/10011) Add a matcher framework for flexible value comparison in scenarios, including semantic datetime matching. + +#### Bug fixes + +- [#10259](https://github.com/microsoft/typespec/pull/10259) Fix query and header matcher handling so matcher objects are compared semantically instead of being serialized to plain strings first. + +### `@typespec/http-server-js` + +#### Bug fixes + +- [#10059](https://github.com/microsoft/typespec/pull/10059) Handle enum types in generated JSON serialization transpose helpers. +- [#10058](https://github.com/microsoft/typespec/pull/10058) Handle immediate scalar-typed and value-literal typed responses in the result-processing layer. + +### Version-bump only in 1.11.0 + +- `@typespec/http` +- `@typespec/json-schema` +- `@typespec/openapi` +- `@typespec/prettier-plugin-typespec` +- `typespec-vscode` +- `typespec-vs` + +
From f671e081f24ddc9265b6e56ccb322d6a5c9ce286 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:14:15 +0000 Subject: [PATCH 12/15] Cascade property.Type to AsParameter for every property in back-compat pass Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/feffcf68-840b-4dd5-8361-53035eddf36b Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index fb0e13aa7be..c8214019ff3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -1288,14 +1288,25 @@ protected internal override IReadOnlyList BuildPropertiesForBa { var newType = lastContractPropertyType.ApplyInputSpecProperty(outputProperty.InputProperty); outputProperty.Type = newType; - // Keep any cached parameters in sync with the overridden property type. - var parameter = outputProperty.AsParameter; - parameter.Update(type: newType); - parameter.ToPublicInputParameter().Update(type: newType.InputType); CodeModelGenerator.Instance.Emitter.Info( $"Changed property '{Name}.{outputProperty.Name}' type to '{lastContractPropertyType}' to match last contract.", BackCompatibilityChangeCategory.PropertyTypePreserved); } + + // Keep any cached parameters in sync with the property type so that + // constructors/methods built before this pass (or by visitors that mutate + // Property.Type without updating the cached ParameterProvider) do not end up + // with a stale parameter type. + var parameter = outputProperty.AsParameter; + if (!parameter.Type.Equals(outputProperty.Type)) + { + parameter.Update(type: outputProperty.Type); + } + var publicInputParameter = parameter.ToPublicInputParameter(); + if (!publicInputParameter.Type.Equals(outputProperty.Type.InputType)) + { + publicInputParameter.Update(type: outputProperty.Type.InputType); + } } return properties; From 7cda7ea40ccc752dff7f946b6fa1624852bcc7ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:36:26 +0000 Subject: [PATCH 13/15] Extract parameter-sync logic into SyncCachedParametersWithPropertyType helper Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/b7e2745a-474f-4b63-876f-d8cb936feb70 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index c8214019ff3..8372e158fa0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -1293,25 +1293,30 @@ protected internal override IReadOnlyList BuildPropertiesForBa BackCompatibilityChangeCategory.PropertyTypePreserved); } - // Keep any cached parameters in sync with the property type so that - // constructors/methods built before this pass (or by visitors that mutate - // Property.Type without updating the cached ParameterProvider) do not end up - // with a stale parameter type. - var parameter = outputProperty.AsParameter; - if (!parameter.Type.Equals(outputProperty.Type)) - { - parameter.Update(type: outputProperty.Type); - } - var publicInputParameter = parameter.ToPublicInputParameter(); - if (!publicInputParameter.Type.Equals(outputProperty.Type.InputType)) - { - publicInputParameter.Update(type: outputProperty.Type.InputType); - } + SyncCachedParametersWithPropertyType(outputProperty); } return properties; } + // Keep any cached parameters in sync with the property type so that + // constructors/methods built before this pass (or by visitors that mutate + // Property.Type without updating the cached ParameterProvider) do not end up + // with a stale parameter type. + private static void SyncCachedParametersWithPropertyType(PropertyProvider outputProperty) + { + var parameter = outputProperty.AsParameter; + if (!parameter.Type.Equals(outputProperty.Type)) + { + parameter.Update(type: outputProperty.Type); + } + var publicInputParameter = parameter.ToPublicInputParameter(); + if (!publicInputParameter.Type.Equals(outputProperty.Type.InputType)) + { + publicInputParameter.Update(type: outputProperty.Type.InputType); + } + } + private bool TryGetLastContractPropertyTypeOverride( PropertyProvider outputProperty, [NotNullWhen(true)] out CSharpType? lastContractPropertyType) From 3df9c80b6afe6c0f32b276ea60af7ed8de863a4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:52:38 +0000 Subject: [PATCH 14/15] Revert unconditional parameter cascade; reset _inputParameter in ParameterProvider.Update Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/e4177a1f-cc04-4b19-b625-12c5c0180923 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 17 ++++++----------- .../src/Providers/ParameterProvider.cs | 4 ++++ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index 8372e158fa0..5a88d7b1b2a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -1291,18 +1291,18 @@ protected internal override IReadOnlyList BuildPropertiesForBa CodeModelGenerator.Instance.Emitter.Info( $"Changed property '{Name}.{outputProperty.Name}' type to '{lastContractPropertyType}' to match last contract.", BackCompatibilityChangeCategory.PropertyTypePreserved); - } - SyncCachedParametersWithPropertyType(outputProperty); + SyncCachedParametersWithPropertyType(outputProperty); + } } return properties; } - // Keep any cached parameters in sync with the property type so that - // constructors/methods built before this pass (or by visitors that mutate - // Property.Type without updating the cached ParameterProvider) do not end up - // with a stale parameter type. + // Keep any cached parameters in sync with the overridden property type so that + // constructors/methods built before this pass do not end up with a stale parameter + // type. Updating the parameter resets its cached public input variant, which will + // be recalculated from the new type on next access. private static void SyncCachedParametersWithPropertyType(PropertyProvider outputProperty) { var parameter = outputProperty.AsParameter; @@ -1310,11 +1310,6 @@ private static void SyncCachedParametersWithPropertyType(PropertyProvider output { parameter.Update(type: outputProperty.Type); } - var publicInputParameter = parameter.ToPublicInputParameter(); - if (!publicInputParameter.Type.Equals(outputProperty.Type.InputType)) - { - publicInputParameter.Update(type: outputProperty.Type.InputType); - } } private bool TryGetLastContractPropertyTypeOverride( diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs index 21e6f9c1adf..69170816543 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs @@ -320,6 +320,10 @@ public void Update( WireInformation? wireInfo = null, ParameterValidationType? validation = null) { + // Reset the cached input variant so that ToPublicInputParameter() recalculates it + // from the updated state rather than returning a stale instance. + _inputParameter = null; + if (name is not null) { Name = name; From 666f586f1f0f43ec28f5d917656290a3a46f39ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 22:21:27 +0000 Subject: [PATCH 15/15] Sync cached parameter type via PropertyProvider.Update instead of helper Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/ff896649-1e81-4fe0-b600-4648e62a3ac0 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 17 +---------------- .../src/Providers/PropertyProvider.cs | 4 ++++ 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index 5a88d7b1b2a..2e8a6076acc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -1287,31 +1287,16 @@ protected internal override IReadOnlyList BuildPropertiesForBa if (TryGetLastContractPropertyTypeOverride(outputProperty, out var lastContractPropertyType)) { var newType = lastContractPropertyType.ApplyInputSpecProperty(outputProperty.InputProperty); - outputProperty.Type = newType; + outputProperty.Update(type: newType); CodeModelGenerator.Instance.Emitter.Info( $"Changed property '{Name}.{outputProperty.Name}' type to '{lastContractPropertyType}' to match last contract.", BackCompatibilityChangeCategory.PropertyTypePreserved); - - SyncCachedParametersWithPropertyType(outputProperty); } } return properties; } - // Keep any cached parameters in sync with the overridden property type so that - // constructors/methods built before this pass do not end up with a stale parameter - // type. Updating the parameter resets its cached public input variant, which will - // be recalculated from the new type on next access. - private static void SyncCachedParametersWithPropertyType(PropertyProvider outputProperty) - { - var parameter = outputProperty.AsParameter; - if (!parameter.Type.Equals(outputProperty.Type)) - { - parameter.Update(type: outputProperty.Type); - } - } - private bool TryGetLastContractPropertyTypeOverride( PropertyProvider outputProperty, [NotNullWhen(true)] out CSharpType? lastContractPropertyType) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs index a7cd7e1a6cc..25a92c7a270 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs @@ -297,6 +297,10 @@ public void Update( if (type != null) { Type = type; + if (_parameter.IsValueCreated && !_parameter.Value.Type.Equals(type)) + { + _parameter.Value.Update(type: type); + } } if (name != null) {