@azure-tools/typespec-ts 0.50.0 → 0.50.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/src/index.d.ts.map +1 -1
  3. package/dist/src/index.js +40 -16
  4. package/dist/src/index.js.map +1 -1
  5. package/dist/src/lib.d.ts +6 -0
  6. package/dist/src/lib.d.ts.map +1 -1
  7. package/dist/src/lib.js +5 -0
  8. package/dist/src/lib.js.map +1 -1
  9. package/dist/src/modular/buildRootIndex.d.ts.map +1 -1
  10. package/dist/src/modular/buildRootIndex.js +19 -33
  11. package/dist/src/modular/buildRootIndex.js.map +1 -1
  12. package/dist/src/modular/buildSubpathIndex.d.ts +7 -0
  13. package/dist/src/modular/buildSubpathIndex.d.ts.map +1 -1
  14. package/dist/src/modular/buildSubpathIndex.js +44 -14
  15. package/dist/src/modular/buildSubpathIndex.js.map +1 -1
  16. package/dist/src/modular/helpers/operationHelpers.d.ts +2 -1
  17. package/dist/src/modular/helpers/operationHelpers.d.ts.map +1 -1
  18. package/dist/src/modular/helpers/operationHelpers.js +108 -14
  19. package/dist/src/modular/helpers/operationHelpers.js.map +1 -1
  20. package/dist/src/modular/static-helpers-metadata.d.ts +17 -0
  21. package/dist/src/modular/static-helpers-metadata.d.ts.map +1 -1
  22. package/dist/src/modular/static-helpers-metadata.js +17 -0
  23. package/dist/src/modular/static-helpers-metadata.js.map +1 -1
  24. package/dist/src/transform/transfromRLCOptions.d.ts.map +1 -1
  25. package/dist/src/transform/transfromRLCOptions.js +3 -1
  26. package/dist/src/transform/transfromRLCOptions.js.map +1 -1
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +2 -2
  29. package/src/index.ts +68 -19
  30. package/src/lib.ts +12 -0
  31. package/src/modular/buildRootIndex.ts +67 -63
  32. package/src/modular/buildSubpathIndex.ts +75 -34
  33. package/src/modular/helpers/operationHelpers.ts +143 -8
  34. package/src/modular/static-helpers-metadata.ts +18 -0
  35. package/src/transform/transfromRLCOptions.ts +3 -1
  36. package/static/static-helpers/storageCompatResponse.ts +90 -0
@@ -9,6 +9,7 @@ import {
9
9
  PagingHelpers,
10
10
  PollingHelpers,
11
11
  SerializationHelpers,
12
+ StorageCompatHelpers,
12
13
  UrlTemplateHelpers,
13
14
  XmlHelpers
14
15
  } from "../static-helpers-metadata.js";
@@ -99,6 +100,17 @@ import {
99
100
  import { isExtensibleEnum } from "../type-expressions/get-enum-expression.js";
100
101
  import { emitInlineModel } from "../type-expressions/get-model-expression.js";
101
102
 
103
+ /**
104
+ * Checks whether a header should be skipped during serialization/deserialization.
105
+ * A header is skipped when it has the "headerCollectionPrefix" client option set,
106
+ * which indicates it uses a prefix-based dictionary pattern not handled by standard ser/deser.
107
+ */
108
+ function shouldSkipHeaderSerialization(
109
+ header: SdkHttpParameter | SdkServiceResponseHeader
110
+ ): boolean {
111
+ return getClientOptions(header, "headerCollectionPrefix") !== undefined;
112
+ }
113
+
102
114
  export function getSendPrivateFunction(
103
115
  dpgContext: SdkContext,
104
116
  method: [string[], ServiceOperation],
@@ -398,7 +410,8 @@ export function getDeserializePrivateFunction(
398
410
 
399
411
  /**
400
412
  * Generates a private function to deserialize response headers.
401
- * Only generated when response headers are present and include-headers-in-response is enabled.
413
+ * Only generated when response headers are present and include-headers-in-response
414
+ * or enable-storage-compat is enabled.
402
415
  */
403
416
  export function getDeserializeHeadersPrivateFunction(
404
417
  context: SdkContext,
@@ -407,9 +420,14 @@ export function getDeserializeHeadersPrivateFunction(
407
420
  const responseHeaders = getResponseHeaders(operation.operation.responses);
408
421
  const isResponseHeadersEnabled =
409
422
  context.rlcOptions?.includeHeadersInResponse === true;
423
+ const isStorageCompatEnabled =
424
+ context.rlcOptions?.enableStorageCompat === true;
410
425
 
411
- // Only generate if headers exist and feature is enabled
412
- if (responseHeaders.length === 0 || !isResponseHeadersEnabled) {
426
+ // Only generate if headers exist and a relevant feature is enabled
427
+ if (
428
+ responseHeaders.length === 0 ||
429
+ (!isResponseHeadersEnabled && !isStorageCompatEnabled)
430
+ ) {
413
431
  return undefined;
414
432
  }
415
433
 
@@ -555,6 +573,7 @@ function getExceptionResponseHeaders(
555
573
  const headerMap = new Map<string, SdkServiceResponseHeader>();
556
574
  for (const exception of exceptions ?? []) {
557
575
  for (const header of exception.headers ?? []) {
576
+ if (shouldSkipHeaderSerialization(header)) continue;
558
577
  const key = header.serializedName ?? header.name;
559
578
  if (!headerMap.has(key)) {
560
579
  headerMap.set(key, header);
@@ -918,6 +937,15 @@ export function getOperationFunction(
918
937
  const hasHeaderOnlyResponse = !response.type && responseHeaders.length > 0;
919
938
  const isResponseHeadersEnabled =
920
939
  context.rlcOptions?.includeHeadersInResponse === true;
940
+ const isStorageCompatEnabled =
941
+ context.rlcOptions?.enableStorageCompat === true;
942
+
943
+ // Track the raw body type separately for storage-compat (before header merging)
944
+ const hasResponseBody = !!response.type;
945
+ let bodyType = "void";
946
+ if (response.type) {
947
+ bodyType = getTypeExpression(context, response.type!);
948
+ }
921
949
 
922
950
  let returnType = { name: "", type: "void" };
923
951
  if (response.type) {
@@ -947,6 +975,37 @@ export function getOperationFunction(
947
975
  type: `${buildHeaderOnlyResponseType(context, responseHeaders)}`
948
976
  };
949
977
  }
978
+
979
+ // When storage-compat is enabled, wrap the return type with StorageCompatResponseInfo
980
+ // Use the raw body type (not the header-augmented return type) for TBody
981
+ let finalReturnType = returnType.type;
982
+ if (isStorageCompatEnabled) {
983
+ const storageCompatInfoRef = resolveReference(
984
+ StorageCompatHelpers.StorageCompatResponseInfo
985
+ );
986
+ const headersType =
987
+ responseHeaders.length > 0
988
+ ? buildHeaderOnlyResponseType(context, responseHeaders)
989
+ : "Record<string, unknown>";
990
+ if (!hasResponseBody) {
991
+ if (responseHeaders.length > 0) {
992
+ // Void with headers — headers at top level + StorageCompatResponseInfo
993
+ finalReturnType = `${headersType} & ${storageCompatInfoRef}<undefined, ${headersType}>`;
994
+ } else {
995
+ // Void without headers — just StorageCompatResponseInfo
996
+ finalReturnType = `${storageCompatInfoRef}<undefined, ${headersType}>`;
997
+ }
998
+ } else {
999
+ if (responseHeaders.length > 0) {
1000
+ // Body with headers — headers + body + StorageCompatResponseInfo at top level
1001
+ finalReturnType = `${headersType} & ${bodyType} & ${storageCompatInfoRef}<${bodyType}, ${headersType}>`;
1002
+ } else {
1003
+ // Body without headers — body + StorageCompatResponseInfo
1004
+ finalReturnType = `${bodyType} & ${storageCompatInfoRef}<${bodyType}, ${headersType}>`;
1005
+ }
1006
+ }
1007
+ }
1008
+
950
1009
  const { name, fixme = [] } = getOperationName(operation);
951
1010
  const functionStatement = {
952
1011
  kind: StructureKind.Function,
@@ -959,7 +1018,7 @@ export function getOperationFunction(
959
1018
  name,
960
1019
  propertyName: normalizeName(operation.name, NameType.Property),
961
1020
  parameters,
962
- returnType: `Promise<${returnType.type}>`
1021
+ returnType: `Promise<${finalReturnType}>`
963
1022
  };
964
1023
 
965
1024
  const statements: string[] = [];
@@ -969,6 +1028,29 @@ export function getOperationFunction(
969
1028
  const resultVarName = generateLocallyUniqueName("result", paramNames);
970
1029
 
971
1030
  const parameterList = parameters.map((p) => p.name).join(", ");
1031
+
1032
+ // When storage-compat is enabled, set up the onResponse interceptor before sending
1033
+ const storageCompatVarName = generateLocallyUniqueName(
1034
+ "_storageCompat",
1035
+ paramNames
1036
+ );
1037
+ if (isStorageCompatEnabled) {
1038
+ const createOnResponseRef = resolveReference(
1039
+ StorageCompatHelpers.createStorageCompatOnResponse
1040
+ );
1041
+ statements.push(
1042
+ `const ${storageCompatVarName} = ${createOnResponseRef}(${optionalParamName}.onResponse);`
1043
+ );
1044
+ }
1045
+
1046
+ // Build the parameterList for the send call, injecting onResponse when storage-compat is enabled
1047
+ const sendParameterList = isStorageCompatEnabled
1048
+ ? parameterList.replace(
1049
+ optionalParamName,
1050
+ `{...${optionalParamName}, onResponse: ${storageCompatVarName}.onResponse}`
1051
+ )
1052
+ : parameterList;
1053
+
972
1054
  // Special case for binary-only bodies: use helper to call streaming methods so that Core doesn't poison the response body by
973
1055
  // doing a UTF-8 decode on the raw bytes.
974
1056
  if (response?.type?.kind === "bytes" && response.type.encode === "bytes") {
@@ -977,19 +1059,55 @@ export function getOperationFunction(
977
1059
  paramNames
978
1060
  );
979
1061
  statements.push(
980
- `const ${streamableMethodVarName} = _${name}Send(${parameterList});`
1062
+ `const ${streamableMethodVarName} = _${name}Send(${sendParameterList});`
981
1063
  );
982
1064
  statements.push(
983
1065
  `const ${resultVarName} = await ${resolveReference(SerializationHelpers.getBinaryResponse)}(${streamableMethodVarName});`
984
1066
  );
985
1067
  } else {
986
1068
  statements.push(
987
- `const ${resultVarName} = await _${name}Send(${parameterList});`
1069
+ `const ${resultVarName} = await _${name}Send(${sendParameterList});`
988
1070
  );
989
1071
  }
990
1072
 
991
1073
  // If the response has headers and the feature flag to include headers in response is enabled, build the headers object and include it in the return value
992
- if (responseHeaders.length > 0 && isResponseHeadersEnabled) {
1074
+ if (isStorageCompatEnabled) {
1075
+ // Storage-compat mode: wrap the return value with _response metadata using captured PipelineResponse
1076
+ const addStorageCompatRef = resolveReference(
1077
+ StorageCompatHelpers.addStorageCompatResponse
1078
+ );
1079
+ const parsedBodyVarName = generateLocallyUniqueName(
1080
+ "parsedBody",
1081
+ paramNames
1082
+ );
1083
+ const parsedHeadersVarName = generateLocallyUniqueName(
1084
+ "parsedHeaders",
1085
+ paramNames
1086
+ );
1087
+
1088
+ // Deserialize body
1089
+ if (!hasResponseBody) {
1090
+ statements.push(`await _${name}Deserialize(${resultVarName});`);
1091
+ } else {
1092
+ statements.push(
1093
+ `const ${parsedBodyVarName} = await _${name}Deserialize(${resultVarName});`
1094
+ );
1095
+ }
1096
+
1097
+ // Deserialize headers if present
1098
+ if (responseHeaders.length > 0) {
1099
+ statements.push(
1100
+ `const ${parsedHeadersVarName} = _${name}DeserializeHeaders(${resultVarName});`
1101
+ );
1102
+ }
1103
+
1104
+ // Build the return statement using captured PipelineResponse
1105
+ const bodyArg = !hasResponseBody ? "undefined" : parsedBodyVarName;
1106
+ const headersArg = responseHeaders.length > 0 ? parsedHeadersVarName : "{}";
1107
+ statements.push(
1108
+ `return ${addStorageCompatRef}(${storageCompatVarName}.getRawResponse()!, ${bodyArg}, ${headersArg});`
1109
+ );
1110
+ } else if (responseHeaders.length > 0 && isResponseHeadersEnabled) {
993
1111
  const headersVarName = generateLocallyUniqueName("headers", paramNames);
994
1112
  statements.push(
995
1113
  `const ${headersVarName} = _${name}DeserializeHeaders(result);`
@@ -1368,6 +1486,10 @@ function getHeaderAndBodyParameters(
1368
1486
  ) {
1369
1487
  continue;
1370
1488
  }
1489
+ // Skip headers marked with headerCollectionPrefix client option
1490
+ if (shouldSkipHeaderSerialization(param)) {
1491
+ continue;
1492
+ }
1371
1493
  // Check if this parameter still exists in the corresponding method params (after override)
1372
1494
  if (
1373
1495
  param.methodParameterSegments &&
@@ -2696,6 +2818,7 @@ export function getResponseHeaders(
2696
2818
  const headerMap = new Map<string, SdkServiceResponseHeader>();
2697
2819
  for (const response of responses ?? []) {
2698
2820
  for (const header of response.headers ?? []) {
2821
+ if (shouldSkipHeaderSerialization(header)) continue;
2699
2822
  const key = header.serializedName ?? header.name;
2700
2823
  if (!headerMap.has(key)) {
2701
2824
  headerMap.set(key, header);
@@ -2720,7 +2843,19 @@ function buildCompositeResponseType(
2720
2843
  ): string {
2721
2844
  const allParents = getAllAncestors(modelType);
2722
2845
  const modelProps: (SdkModelPropertyType | SdkServiceResponseHeader)[] =
2723
- getAllProperties(context, modelType, allParents);
2846
+ getAllProperties(context, modelType, allParents).filter((property) => {
2847
+ // Skip model properties that are headers with headerCollectionPrefix
2848
+ if (
2849
+ property.__raw &&
2850
+ isHeader(context.program, property.__raw) &&
2851
+ shouldSkipHeaderSerialization(
2852
+ property as SdkModelPropertyType & SdkServiceResponseHeader
2853
+ )
2854
+ ) {
2855
+ return false;
2856
+ }
2857
+ return true;
2858
+ });
2724
2859
 
2725
2860
  // Collect header property names already in the model to avoid duplicates
2726
2861
  const modelHeaderNames = new Set<string>();
@@ -235,3 +235,21 @@ export const XmlHelpers = {
235
235
  location: "serialization/xml-helpers.ts"
236
236
  }
237
237
  } as const;
238
+
239
+ export const StorageCompatHelpers = {
240
+ StorageCompatResponseInfo: {
241
+ kind: "interface",
242
+ name: "StorageCompatResponseInfo",
243
+ location: "storageCompatResponse.ts"
244
+ },
245
+ createStorageCompatOnResponse: {
246
+ kind: "function",
247
+ name: "createStorageCompatOnResponse",
248
+ location: "storageCompatResponse.ts"
249
+ },
250
+ addStorageCompatResponse: {
251
+ kind: "function",
252
+ name: "addStorageCompatResponse",
253
+ location: "storageCompatResponse.ts"
254
+ }
255
+ } as const;
@@ -92,6 +92,7 @@ function extractRLCOptions(
92
92
  emitterOptions["ignore-enum-member-name-normalize"];
93
93
  const compatibilityQueryMultiFormat =
94
94
  emitterOptions["compatibility-query-multi-format"];
95
+ const enableStorageCompat = emitterOptions["enable-storage-compat"] === true;
95
96
  const typespecTitleMap = emitterOptions["typespec-title-map"];
96
97
  const hasSubscriptionId = getSubscriptionId(dpgContext);
97
98
  const ignoreNullableOnOptional = getIgnoreNullableOnOptional(
@@ -134,7 +135,8 @@ function extractRLCOptions(
134
135
  ignoreEnumMemberNameNormalize,
135
136
  hasSubscriptionId,
136
137
  ignoreNullableOnOptional,
137
- isMultiService
138
+ isMultiService,
139
+ enableStorageCompat
138
140
  };
139
141
  }
140
142
 
@@ -0,0 +1,90 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+
4
+ import type {
5
+ FullOperationResponse,
6
+ RawResponseCallback
7
+ } from "@azure-rest/core-client";
8
+
9
+ /**
10
+ * Response metadata providing access to raw HTTP response details.
11
+ * Available when the `enable-storage-compat` emitter option is enabled.
12
+ */
13
+ export interface StorageCompatResponseInfo<
14
+ TBody = unknown,
15
+ THeaders = Record<string, unknown>
16
+ > {
17
+ _response: {
18
+ /** The raw FullOperationResponse from the HTTP pipeline. */
19
+ rawResponse: FullOperationResponse;
20
+ /** The deserialized response body. */
21
+ parsedBody: TBody;
22
+ /** The deserialized response headers. */
23
+ parsedHeaders: THeaders;
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Creates an onResponse callback that captures the raw FullOperationResponse.
29
+ * Chains with any existing user-provided onResponse callback.
30
+ * @param originalOnResponse - The user's original onResponse callback, if any.
31
+ * @returns An object with the onResponse callback and a getter for the captured response.
32
+ */
33
+ export function createStorageCompatOnResponse(
34
+ originalOnResponse?: RawResponseCallback
35
+ ): {
36
+ onResponse: RawResponseCallback;
37
+ getRawResponse: () => FullOperationResponse | undefined;
38
+ } {
39
+ let captured: FullOperationResponse | undefined;
40
+ return {
41
+ onResponse: (rawResponse: FullOperationResponse, error?: unknown) => {
42
+ captured = rawResponse;
43
+ originalOnResponse?.(rawResponse, error);
44
+ },
45
+ getRawResponse: () => captured
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Computes the return type for addStorageCompatResponse:
51
+ * - When TBody is undefined/void/null, returns THeaders & StorageCompatResponseInfo
52
+ * - Otherwise returns THeaders & TBody & StorageCompatResponseInfo
53
+ *
54
+ * Headers and body properties are spread at the top level of the result,
55
+ * in addition to being available under `_response.parsedHeaders` / `_response.parsedBody`.
56
+ */
57
+ type StorageCompatResult<TBody, THeaders> = TBody extends
58
+ | undefined
59
+ | void
60
+ | null
61
+ ? THeaders & StorageCompatResponseInfo<TBody, THeaders>
62
+ : THeaders & TBody & StorageCompatResponseInfo<TBody, THeaders>;
63
+
64
+ /**
65
+ * Augments a deserialized response with raw HTTP response metadata.
66
+ * @param rawResponse - The raw FullOperationResponse from the HTTP pipeline.
67
+ * @param parsedBody - The deserialized response body.
68
+ * @param parsedHeaders - The deserialized response headers.
69
+ * @returns The parsedBody augmented with a `_response` property.
70
+ */
71
+ export function addStorageCompatResponse<
72
+ TBody,
73
+ THeaders = Record<string, unknown>
74
+ >(
75
+ rawResponse: FullOperationResponse,
76
+ parsedBody: TBody,
77
+ parsedHeaders: THeaders
78
+ ): StorageCompatResult<TBody, THeaders> {
79
+ const base =
80
+ parsedBody !== undefined && parsedBody !== null
81
+ ? parsedBody
82
+ : ({} as TBody);
83
+ return Object.assign(base as any, parsedHeaders, {
84
+ _response: {
85
+ rawResponse,
86
+ parsedBody,
87
+ parsedHeaders
88
+ }
89
+ });
90
+ }