@azure-tools/typespec-ts 0.51.0 → 0.51.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 (41) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/src/index.d.ts.map +1 -1
  3. package/dist/src/index.js +8 -4
  4. package/dist/src/index.js.map +1 -1
  5. package/dist/src/lib.d.ts +10 -3
  6. package/dist/src/lib.d.ts.map +1 -1
  7. package/dist/src/lib.js +6 -1
  8. package/dist/src/lib.js.map +1 -1
  9. package/dist/src/modular/emitModels.d.ts.map +1 -1
  10. package/dist/src/modular/emitModels.js +45 -11
  11. package/dist/src/modular/emitModels.js.map +1 -1
  12. package/dist/src/modular/helpers/operationHelpers.d.ts.map +1 -1
  13. package/dist/src/modular/helpers/operationHelpers.js +65 -43
  14. package/dist/src/modular/helpers/operationHelpers.js.map +1 -1
  15. package/dist/src/modular/static-helpers-metadata.d.ts +3 -3
  16. package/dist/src/modular/static-helpers-metadata.js +3 -3
  17. package/dist/src/modular/static-helpers-metadata.js.map +1 -1
  18. package/dist/src/modular/type-expressions/get-type-expression.d.ts.map +1 -1
  19. package/dist/src/modular/type-expressions/get-type-expression.js +4 -1
  20. package/dist/src/modular/type-expressions/get-type-expression.js.map +1 -1
  21. package/dist/src/transform/transfromRLCOptions.d.ts.map +1 -1
  22. package/dist/src/transform/transfromRLCOptions.js +3 -1
  23. package/dist/src/transform/transfromRLCOptions.js.map +1 -1
  24. package/dist/src/utils/clientUtils.d.ts.map +1 -1
  25. package/dist/src/utils/clientUtils.js +4 -0
  26. package/dist/src/utils/clientUtils.js.map +1 -1
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +12 -12
  29. package/src/index.ts +6 -0
  30. package/src/lib.ts +14 -1
  31. package/src/modular/emitModels.ts +52 -13
  32. package/src/modular/helpers/operationHelpers.ts +72 -59
  33. package/src/modular/static-helpers-metadata.ts +3 -3
  34. package/src/modular/type-expressions/get-type-expression.ts +3 -1
  35. package/src/transform/transfromRLCOptions.ts +4 -1
  36. package/src/utils/clientUtils.ts +4 -0
  37. package/static/static-helpers/serialization/get-binary-stream-response-browser.mts +23 -0
  38. package/static/static-helpers/serialization/get-binary-stream-response.ts +23 -0
  39. package/static/static-helpers/serialization/xml-helpers.ts +1 -1
  40. package/static/static-helpers/serialization/get-binary-response-body-browser.mts +0 -22
  41. package/static/static-helpers/serialization/get-binary-response-body.ts +0 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-tools/typespec-ts",
3
- "version": "0.51.0",
3
+ "version": "0.51.1",
4
4
  "description": "An experimental TypeSpec emitter for TypeScript RLC",
5
5
  "main": "dist/src/index.js",
6
6
  "type": "module",
@@ -18,15 +18,15 @@
18
18
  "license": "MIT",
19
19
  "devDependencies": {
20
20
  "@azure-rest/core-client": "^2.3.1",
21
- "@typespec/http-specs": "0.1.0-alpha.35-dev.1",
22
- "@typespec/spector": "0.1.0-dev.0",
23
- "@typespec/spec-api": "0.1.0-alpha.14-dev.1",
21
+ "@typespec/http-specs": "0.1.0-dev.2",
22
+ "@typespec/spector": "0.1.0-dev.3",
23
+ "@typespec/spec-api": "0.1.0-alpha.14-dev.3",
24
24
  "@typespec/tspd": "0.74.1",
25
- "@azure-tools/azure-http-specs": "0.1.0-alpha.39-dev.2",
26
- "@azure-tools/typespec-autorest": "^0.66.0",
27
- "@azure-tools/typespec-azure-core": "^0.66.0",
28
- "@azure-tools/typespec-azure-resource-manager": "^0.66.0",
29
- "@azure-tools/typespec-client-generator-core": "^0.66.2",
25
+ "@azure-tools/azure-http-specs": "0.1.0-alpha.39-dev.7",
26
+ "@azure-tools/typespec-autorest": "^0.66.1",
27
+ "@azure-tools/typespec-azure-core": "^0.66.1",
28
+ "@azure-tools/typespec-azure-resource-manager": "^0.66.1",
29
+ "@azure-tools/typespec-client-generator-core": "^0.66.4",
30
30
  "@azure/abort-controller": "^2.1.2",
31
31
  "@azure/core-auth": "^1.6.0",
32
32
  "@azure/core-lro": "^3.1.0",
@@ -64,8 +64,8 @@
64
64
  "js-yaml": "^4.1.0"
65
65
  },
66
66
  "peerDependencies": {
67
- "@azure-tools/typespec-azure-core": "^0.66.0",
68
- "@azure-tools/typespec-client-generator-core": "^0.66.2",
67
+ "@azure-tools/typespec-azure-core": "^0.66.1",
68
+ "@azure-tools/typespec-client-generator-core": "^0.66.4",
69
69
  "@typespec/compiler": "^1.10.0",
70
70
  "@typespec/http": "^1.10.0",
71
71
  "@typespec/rest": "^0.80.0",
@@ -73,7 +73,7 @@
73
73
  "@typespec/xml": "^0.80.0"
74
74
  },
75
75
  "dependencies": {
76
- "@azure-tools/rlc-common": "^0.51.0",
76
+ "@azure-tools/rlc-common": "^0.51.1",
77
77
  "fast-xml-parser": "^4.5.0",
78
78
  "fs-extra": "^11.1.0",
79
79
  "lodash": "^4.17.21",
package/src/index.ts CHANGED
@@ -124,6 +124,12 @@ export async function $onEmit(context: EmitContext) {
124
124
  const program: Program = context.program;
125
125
  const emitterOptions: EmitterOptions = context.options;
126
126
  const dpgContext = await createContextWithDefaultOptions(context);
127
+
128
+ // Report any diagnostics from TCGC
129
+ if (dpgContext.diagnostics?.length > 0) {
130
+ program.reportDiagnostics(dpgContext.diagnostics);
131
+ }
132
+
127
133
  // Enrich the dpg context with path detail and common options
128
134
  await enrichDpgContext();
129
135
  const rlcOptions = dpgContext.rlcOptions ?? {};
package/src/lib.ts CHANGED
@@ -92,6 +92,13 @@ export interface EmitterOptions {
92
92
  * Defaults to `false`.
93
93
  */
94
94
  "enable-storage-compat"?: boolean;
95
+ /**
96
+ * When set to true, TypeSpec `unknown` type will be translated to `Record<string, unknown>`
97
+ * instead of `any` in the generated Modular SDK. This is useful when migrating from HLC
98
+ * where `unknown` in swagger mapped to `Record<string, unknown>` in the SDK.
99
+ * (Modular SDK only) Defaults to `false`.
100
+ */
101
+ "treat-unknown-as-record"?: boolean;
95
102
  }
96
103
 
97
104
  export const RLCOptionsSchema: JSONSchemaType<EmitterOptions> = {
@@ -388,6 +395,12 @@ export const RLCOptionsSchema: JSONSchemaType<EmitterOptions> = {
388
395
  nullable: true,
389
396
  description:
390
397
  "When enabled, every regular (non-LRO, non-paging) operation return type is augmented with a `_response` property containing `rawResponse` (PathUncheckedResponse), `parsedBody`, and `parsedHeaders`. Defaults to `false`."
398
+ },
399
+ "treat-unknown-as-record": {
400
+ type: "boolean",
401
+ nullable: true,
402
+ description:
403
+ "When set to true, TypeSpec `unknown` type will be translated to `Record<string, unknown>` instead of `any` in the generated Modular SDK. This is useful when migrating from HLC where `unknown` in swagger mapped to `Record<string, unknown>`. (Modular SDK only) Defaults to `false`."
391
404
  }
392
405
  },
393
406
  required: []
@@ -573,7 +586,7 @@ const libDef = {
573
586
  "prefix-adding-in-enum-member": {
574
587
  severity: "warning",
575
588
  messages: {
576
- default: paramMessage`Enum member name ${"memberName"} is normalized to ${"normalizedName"} with "_" prefix.`
589
+ default: paramMessage`Enum member name ${"memberName"} is not a valid TypeScript identifier. It has been renamed to ${"normalizedName"} using the enum type name ${"enumTypeName"} as prefix.`
577
590
  }
578
591
  },
579
592
  "default-value-object": {
@@ -597,13 +597,14 @@ export function buildEnumTypes(
597
597
  type: SdkEnumType,
598
598
  reportMemberNameDiagnostic = false // if reportMemberNameDiagnostic is true, it will report diagnostic for enum member name
599
599
  ): [TypeAliasDeclarationStructure, EnumDeclarationStructure] {
600
+ const rawMembers = type.values.map((value) =>
601
+ emitEnumMember(context, value, reportMemberNameDiagnostic)
602
+ );
600
603
  const enumDeclaration: EnumDeclarationStructure = {
601
604
  kind: StructureKind.Enum,
602
605
  name: `Known${normalizeModelName(context, type)}`,
603
606
  isExported: true,
604
- members: type.values.map((value) =>
605
- emitEnumMember(context, value, reportMemberNameDiagnostic)
606
- )
607
+ members: deduplicateEnumMemberNames(rawMembers)
607
608
  };
608
609
 
609
610
  const enumAsUnion: TypeAliasDeclarationStructure = {
@@ -650,27 +651,65 @@ function getExtensibleEnumDescription(
650
651
  ].join(" \n");
651
652
  }
652
653
 
654
+ /**
655
+ * Deduplicates enum member names by appending a numeric suffix (_1, _2, ...)
656
+ * to all members that share the same normalized name.
657
+ * This handles cases where different values (e.g. "10" and "1.0") normalize
658
+ * to the same identifier.
659
+ */
660
+ function deduplicateEnumMemberNames(
661
+ members: EnumMemberStructure[]
662
+ ): EnumMemberStructure[] {
663
+ const nameCount = new Map<string, number>();
664
+ for (const member of members) {
665
+ const name = member.name as string;
666
+ nameCount.set(name, (nameCount.get(name) ?? 0) + 1);
667
+ }
668
+ const nameCurrentIndex = new Map<string, number>();
669
+ return members.map((member) => {
670
+ const name = member.name as string;
671
+ if ((nameCount.get(name) ?? 0) > 1) {
672
+ const index = (nameCurrentIndex.get(name) ?? 0) + 1;
673
+ nameCurrentIndex.set(name, index);
674
+ return { ...member, name: `${name}_${index}` };
675
+ }
676
+ return member;
677
+ });
678
+ }
679
+
653
680
  function emitEnumMember(
654
681
  context: SdkContext,
655
682
  member: SdkEnumValueType,
656
683
  reportMemberNameDiagnostic = false // if reportMemberNameDiagnostic is true, it will report diagnostic for enum member name
657
684
  ): EnumMemberStructure {
658
- const normalizedMemberName = context.rlcOptions?.ignoreEnumMemberNameNormalize
685
+ const shouldNormalizeName = !member.name.startsWith("$DO_NOT_NORMALIZE$");
686
+ const enumTypeName = normalizeName(
687
+ member.enumType.name,
688
+ NameType.Interface,
689
+ true
690
+ );
691
+ let normalizedMemberName = context.rlcOptions?.ignoreEnumMemberNameNormalize
659
692
  ? fixLeadingNumber(member.name, NameType.EnumMemberName) // need to fix the leading number also for enum member
660
693
  : normalizeName(member.name, NameType.EnumMemberName, true);
694
+ // If the member name starts with _ due to a leading digit (not because the original has _),
695
+ // replace the _ prefix with the enum type name for a more descriptive identifier
661
696
  if (
662
- reportMemberNameDiagnostic &&
697
+ shouldNormalizeName &&
663
698
  normalizedMemberName.toLowerCase().startsWith("_") &&
664
699
  !member.name.toLowerCase().startsWith("_")
665
700
  ) {
666
- reportDiagnostic(context.program, {
667
- code: "prefix-adding-in-enum-member",
668
- format: {
669
- memberName: member.name,
670
- normalizedName: normalizedMemberName
671
- },
672
- target: NoTarget
673
- });
701
+ normalizedMemberName = enumTypeName + normalizedMemberName.slice(1);
702
+ if (reportMemberNameDiagnostic) {
703
+ reportDiagnostic(context.program, {
704
+ code: "prefix-adding-in-enum-member",
705
+ format: {
706
+ memberName: member.name,
707
+ normalizedName: normalizedMemberName,
708
+ enumTypeName
709
+ },
710
+ target: NoTarget
711
+ });
712
+ }
674
713
  }
675
714
  const memberStructure: EnumMemberStructure = {
676
715
  kind: StructureKind.EnumMember,
@@ -191,17 +191,9 @@ export function getDeserializePrivateFunction(
191
191
 
192
192
  // Check if we need to wrap the non-model return type
193
193
  const { shouldWrap, isBinary } = checkWrapNonModelReturn(context, operation);
194
- // For binary wrap, the deserializer receives a StreamableMethod directly
195
- const resultParamType =
196
- shouldWrap && isBinary
197
- ? resolveReference(dependencies.StreamableMethod)
198
- : PathUncheckedResponseReference;
199
- const parameters: OptionalKind<ParameterDeclarationStructure>[] = [
200
- {
201
- name: "result",
202
- type: resultParamType
203
- }
204
- ];
194
+ // For binary wrap, the deserializer receives PathUncheckedResponse & { blobBody, readableStreamBody }
195
+ // which is returned by getBinaryStreamResponse.
196
+ const isBinaryWrap = shouldWrap && isBinary;
205
197
  const isLroOnly = isLroOnlyOperation(operation);
206
198
  const isLroAndPaging = isLroAndPagingOperation(operation);
207
199
  const isPagingOnly = isPagingOnlyOperation(operation);
@@ -236,6 +228,16 @@ export function getDeserializePrivateFunction(
236
228
  returnType = { name: "", type: "void" };
237
229
  }
238
230
 
231
+ const resultParamName = "result";
232
+ const resultParamType = isBinaryWrap
233
+ ? `${PathUncheckedResponseReference} & ${returnType.type}`
234
+ : PathUncheckedResponseReference;
235
+ const parameters: OptionalKind<ParameterDeclarationStructure>[] = [
236
+ {
237
+ name: resultParamName,
238
+ type: resultParamType
239
+ }
240
+ ];
239
241
  const functionStatement: OptionalKind<FunctionDeclarationStructure> = {
240
242
  isAsync: true,
241
243
  isExported: true,
@@ -247,17 +249,14 @@ export function getDeserializePrivateFunction(
247
249
  const createRestErrorReference = resolveReference(
248
250
  dependencies.createRestError
249
251
  );
250
- // For binary wrap, parameter is StreamableMethod - skip status check
251
- if (!(shouldWrap && isBinary)) {
252
- statements.push(
253
- `const expectedStatuses = ${getExpectedStatuses(operation)};`
254
- );
255
- statements.push(
256
- `if(!expectedStatuses.includes(result.status)){`,
257
- `${getExceptionThrowStatement(context, operation)}`,
258
- "}"
259
- );
260
- }
252
+ statements.push(
253
+ `const expectedStatuses = ${getExpectedStatuses(operation)};`
254
+ );
255
+ statements.push(
256
+ `if(!expectedStatuses.includes(result.status)){`,
257
+ `${getExceptionThrowStatement(context, operation)}`,
258
+ "}"
259
+ );
261
260
  const deserializedType =
262
261
  isLroOnly || isLroAndPaging
263
262
  ? operation?.lroMetadata?.finalResponse?.result
@@ -396,19 +395,11 @@ export function getDeserializePrivateFunction(
396
395
  // Handle wrap-non-model-return for non-LRO, non-paging operations
397
396
  if (shouldWrap) {
398
397
  if (isBinary) {
399
- const getBinaryResponseBodyReference = resolveReference(
400
- SerializationHelpers.getBinaryResponseBody
401
- );
402
- const deserializeError = getExceptionDetails(
403
- context,
404
- operation
405
- ).defaultDeserializer;
406
- const deserializeErrorStr = deserializeError
407
- ? `, ${deserializeError}`
408
- : "";
398
+ // Binary wrap: getBinaryStream already resolved the stream,
399
+ // status check and error.details handling ran above.
400
+ // Return the platform-specific stream properties.
409
401
  statements.push(
410
- `return ${getBinaryResponseBodyReference}(result, ${getExpectedStatuses(operation)}${deserializeErrorStr});
411
- `
402
+ `return { blobBody: result.blobBody, readableStreamBody: result.readableStreamBody };`
412
403
  );
413
404
  } else {
414
405
  // Non-model response: wrap with body property
@@ -913,7 +904,8 @@ function getOperationSignatureParameters(
913
904
  !(
914
905
  p.isGeneratedName &&
915
906
  (p.name === "contentType" || p.name === "accept")
916
- ) // skip tcgc generated contentType and accept header parameter
907
+ ) && // skip tcgc generated contentType and accept header parameter
908
+ getClientOptions(p, "headerCollectionPrefix") === undefined // skip headers with collection prefix
917
909
  )
918
910
  .map((p) => {
919
911
  return {
@@ -1011,6 +1003,7 @@ export function getOperationFunction(
1011
1003
  name: getOperationResponseTypeName(method),
1012
1004
  type: resolveReference(refkey(operation, "response"))
1013
1005
  };
1006
+ bodyType = returnType.type;
1014
1007
  } else if (response.type) {
1015
1008
  const type = response.type;
1016
1009
 
@@ -1091,23 +1084,6 @@ export function getOperationFunction(
1091
1084
  const resultVarName = generateLocallyUniqueName("result", paramNames);
1092
1085
 
1093
1086
  const parameterList = parameters.map((p) => p.name).join(", ");
1094
- // For binary wrap, pass the StreamableMethod directly to the deserializer.
1095
- // The deserializer uses asBrowserStream()/asNodeStream() to build the wrapper.
1096
- if (wrapReturn && wrapReturnIsBinary) {
1097
- const streamableMethodVarName = generateLocallyUniqueName(
1098
- "streamableMethod",
1099
- paramNames
1100
- );
1101
- statements.push(
1102
- `const ${streamableMethodVarName} = _${name}Send(${parameterList});`
1103
- );
1104
- statements.push(`return _${name}Deserialize(${streamableMethodVarName});`);
1105
- return {
1106
- ...functionStatement,
1107
- statements
1108
- } as FunctionDeclarationStructure & { propertyName?: string };
1109
- }
1110
-
1111
1087
  // When storage-compat is enabled, set up the onResponse interceptor before sending
1112
1088
  const storageCompatVarName = generateLocallyUniqueName(
1113
1089
  "_storageCompat",
@@ -1132,6 +1108,8 @@ export function getOperationFunction(
1132
1108
 
1133
1109
  // Special case for binary-only bodies: use helper to call streaming methods so that Core doesn't poison the response body by
1134
1110
  // doing a UTF-8 decode on the raw bytes.
1111
+ // For binary wrap, use getBinaryStreamResponse which preserves blobBody/readableStreamBody properties.
1112
+ // For non-wrapped binary, use getBinaryResponse which buffers the body into Uint8Array.
1135
1113
  if (response?.type?.kind === "bytes" && response.type.encode === "bytes") {
1136
1114
  const streamableMethodVarName = generateLocallyUniqueName(
1137
1115
  "streamableMethod",
@@ -1140,8 +1118,12 @@ export function getOperationFunction(
1140
1118
  statements.push(
1141
1119
  `const ${streamableMethodVarName} = _${name}Send(${sendParameterList});`
1142
1120
  );
1121
+ const binaryHelper =
1122
+ wrapReturn && wrapReturnIsBinary
1123
+ ? SerializationHelpers.getBinaryStreamResponse
1124
+ : SerializationHelpers.getBinaryResponse;
1143
1125
  statements.push(
1144
- `const ${resultVarName} = await ${resolveReference(SerializationHelpers.getBinaryResponse)}(${streamableMethodVarName});`
1126
+ `const ${resultVarName} = await ${resolveReference(binaryHelper)}(${streamableMethodVarName});`
1145
1127
  );
1146
1128
  } else {
1147
1129
  statements.push(
@@ -1773,7 +1755,7 @@ export function getParameterMap(
1773
1755
  ? getHeaderSerializedName(param)
1774
1756
  : getPropertySerializedName(param);
1775
1757
 
1776
- if (isConstant(param.type)) {
1758
+ if (isConstant(param.type) && !isOptional(param)) {
1777
1759
  return `"${serializedName}": ${getConstantValue(param.type)}`;
1778
1760
  }
1779
1761
 
@@ -3081,17 +3063,48 @@ export function checkWrapNonModelReturn(
3081
3063
  const type = response.type;
3082
3064
  const contentTypes = operation.operation.responses[0]?.contentTypes ?? [];
3083
3065
 
3084
- // Check if it's a binary (bytes with binary content type) response
3066
+ // Determine whether to wrap based on the HLC PropertyKind mapping.
3067
+ // HLC wraps with `body` only when the type is PropertyKind.Primitive or PropertyKind.Enum
3068
+ // (i.e. NOT PropertyKind.Composite or PropertyKind.Dictionary).
3069
+ // For array types, HLC uses the item's kind, not the array itself.
3070
+ //
3071
+ // Case: bytes with binary content type → binary wrap (isBinary=true)
3072
+ // HLC: bytes → binary payload → separate binary handling
3085
3073
  if (type.__raw && isBinaryPayload(context, type.__raw, contentTypes)) {
3086
3074
  return { shouldWrap: true, isBinary: true };
3087
3075
  }
3088
3076
 
3089
- // Check if it's a non-model, non-record type (array, scalar, enum, etc.)
3090
- if (type.kind !== "model" && type.kind !== "dict") {
3091
- return { shouldWrap: true, isBinary: false };
3077
+ // Case: model array (e.g. Foo[]) no wrap
3078
+ // HLC: model array → PropertyKind.Composite (item kind = model) no body wrapper
3079
+ // Modular: array with valueType.kind === "model"
3080
+ if (type.kind === "array" && type.valueType.kind === "model") {
3081
+ return noWrap;
3082
+ }
3083
+
3084
+ // Case: any object / Record (e.g. Record<string, unknown>) → no wrap
3085
+ // HLC: SchemaType.AnyObject → PropertyKind.Dictionary → no body wrapper
3086
+ // Modular: kind === "dict"
3087
+ // Case: model → no wrap
3088
+ // HLC: model → PropertyKind.Composite → no body wrapper
3089
+ // Modular: kind === "model"
3090
+ if (type.kind === "dict" || type.kind === "model") {
3091
+ return noWrap;
3092
+ }
3093
+
3094
+ // Case: unknown with treatUnknownAsRecord enabled → no wrap
3095
+ // When treatUnknownAsRecord is enabled, `unknown` is treated as Record<string, unknown>
3096
+ // which maps to HLC PropertyKind.Dictionary → no body wrapper
3097
+ if (type.kind === "unknown" && context.rlcOptions?.treatUnknownAsRecord) {
3098
+ return noWrap;
3092
3099
  }
3093
3100
 
3094
- return noWrap;
3101
+ // Remaining cases → wrap with `body`:
3102
+ // - string → HLC PropertyKind.Primitive → modular `string`
3103
+ // - boolean → HLC PropertyKind.Primitive → modular `boolean`
3104
+ // - string[] → HLC PropertyKind.Primitive (array keeps item kind) → modular `string[]`
3105
+ // - any → HLC PropertyKind.Primitive → modular `unknown` or `any`
3106
+ // - enum → HLC PropertyKind.Enum → modular `KnownXxx | string`
3107
+ return { shouldWrap: true, isBinary: false };
3095
3108
  }
3096
3109
 
3097
3110
  /**
@@ -59,10 +59,10 @@ export const SerializationHelpers = {
59
59
  name: "getBinaryResponse",
60
60
  location: "serialization/get-binary-response.ts"
61
61
  },
62
- getBinaryResponseBody: {
62
+ getBinaryStreamResponse: {
63
63
  kind: "function",
64
- name: "getBinaryResponseBody",
65
- location: "serialization/get-binary-response-body.ts"
64
+ name: "getBinaryStreamResponse",
65
+ location: "serialization/get-binary-stream-response.ts"
66
66
  },
67
67
  areAllPropsUndefined: {
68
68
  kind: "function",
@@ -40,7 +40,9 @@ export function getTypeExpression(
40
40
  case "enum":
41
41
  return getEnumExpression(context, type);
42
42
  case "unknown":
43
- return "any";
43
+ return context.rlcOptions?.treatUnknownAsRecord
44
+ ? "Record<string, unknown>"
45
+ : "any";
44
46
  case "boolean":
45
47
  return "boolean";
46
48
  case "decimal":
@@ -93,6 +93,8 @@ function extractRLCOptions(
93
93
  const compatibilityQueryMultiFormat =
94
94
  emitterOptions["compatibility-query-multi-format"];
95
95
  const enableStorageCompat = emitterOptions["enable-storage-compat"] === true;
96
+ const treatUnknownAsRecord =
97
+ emitterOptions["treat-unknown-as-record"] === true;
96
98
  const typespecTitleMap = emitterOptions["typespec-title-map"];
97
99
  const hasSubscriptionId = getSubscriptionId(dpgContext);
98
100
  const ignoreNullableOnOptional = getIgnoreNullableOnOptional(
@@ -138,7 +140,8 @@ function extractRLCOptions(
138
140
  ignoreNullableOnOptional,
139
141
  wrapNonModelReturn,
140
142
  isMultiService,
141
- enableStorageCompat
143
+ enableStorageCompat,
144
+ treatUnknownAsRecord
142
145
  };
143
146
  }
144
147
 
@@ -58,6 +58,8 @@ export function getRLCClients(
58
58
  service: service,
59
59
  type: service,
60
60
  services: [service],
61
+ subClients: [],
62
+ clientPath: clientName,
61
63
  arm: Boolean(dpgContext.arm),
62
64
  crossLanguageDefinitionId: `${getNamespaceFullName(
63
65
  service
@@ -77,6 +79,8 @@ export function getRLCClients(
77
79
  service: service,
78
80
  type: service,
79
81
  services: [service],
82
+ subClients: [],
83
+ clientPath: clientName,
80
84
  arm: Boolean(dpgContext.arm),
81
85
  crossLanguageDefinitionId: `${getNamespaceFullName(
82
86
  service
@@ -0,0 +1,23 @@
1
+ import { HttpResponse, StreamableMethod } from "@azure-rest/core-client";
2
+
3
+ /**
4
+ * Resolves a StreamableMethod into a binary stream response using browser streaming.
5
+ * Returns both the raw HttpResponse (for status/header inspection) and a blobBody Promise.
6
+ * Error handling is left to the caller so that generated deserializers can apply
7
+ * operation-specific error deserialization (per-status-code details, exception headers, etc.).
8
+ */
9
+ export async function getBinaryStreamResponse(
10
+ streamableMethod: StreamableMethod
11
+ ): Promise<
12
+ HttpResponse & {
13
+ blobBody?: Promise<Blob>;
14
+ readableStreamBody?: NodeJS.ReadableStream;
15
+ }
16
+ > {
17
+ const response = await streamableMethod.asBrowserStream();
18
+ return {
19
+ ...response,
20
+ blobBody: new Response(response.body).blob(),
21
+ readableStreamBody: undefined,
22
+ };
23
+ }
@@ -0,0 +1,23 @@
1
+ import { HttpResponse, StreamableMethod } from "@azure-rest/core-client";
2
+
3
+ /**
4
+ * Resolves a StreamableMethod into a binary stream response using Node.js streaming.
5
+ * Returns both the raw HttpResponse (for status/header inspection) and the readable stream body.
6
+ * Error handling is left to the caller so that generated deserializers can apply
7
+ * operation-specific error deserialization (per-status-code details, exception headers, etc.).
8
+ */
9
+ export async function getBinaryStreamResponse(
10
+ streamableMethod: StreamableMethod
11
+ ): Promise<
12
+ HttpResponse & {
13
+ blobBody?: Promise<Blob>;
14
+ readableStreamBody?: NodeJS.ReadableStream;
15
+ }
16
+ > {
17
+ const response = await streamableMethod.asNodeStream();
18
+ return {
19
+ ...response,
20
+ blobBody: undefined,
21
+ readableStreamBody: response.body
22
+ };
23
+ }
@@ -108,7 +108,7 @@ const defaultParserOptions = {
108
108
  trimValues: false, // Preserve whitespace in text content
109
109
  isArray: (
110
110
  _name: string,
111
- _jpath: string,
111
+ _jpath: string | unknown,
112
112
  isLeafNode: boolean,
113
113
  isAttribute: boolean
114
114
  ) => {
@@ -1,22 +0,0 @@
1
- import { createRestError, StreamableMethod } from "@azure-rest/core-client";
2
-
3
- /**
4
- * Get the binary response body from a StreamableMethod, returning it as a Blob in browser environments and as a NodeJS.ReadableStream in Node.js environments. This function checks the response status against expected statuses and throws a RestError if the status is unexpected, optionally deserializing error details using a provided deserialization function.
5
- */
6
- export async function getBinaryResponseBody(
7
- result: StreamableMethod,
8
- expectedStatuses: string[] = ["200"],
9
- deserializeError?: (error: any) => unknown
10
- ): Promise<{ blobBody?: Promise<Blob>; readableStreamBody?: NodeJS.ReadableStream }> {
11
- const browserStream = await result.asBrowserStream();
12
- if (!expectedStatuses.includes(browserStream.status)) {
13
- const error = createRestError(browserStream);
14
- if (deserializeError) {
15
- error.details = deserializeError(browserStream.body);
16
- }
17
-
18
- throw error;
19
- }
20
- return { blobBody: new Response(browserStream.body).blob(), readableStreamBody: undefined };
21
-
22
- }
@@ -1,24 +0,0 @@
1
- import { createRestError, StreamableMethod } from "@azure-rest/core-client";
2
-
3
- /**
4
- * Gets the binary response body from a StreamableMethod, returning it as a NodeJS.ReadableStream in Node.js environments. In browser environments, this function returns `undefined` for the stream and instead provides a `blobBody` that is a Promise resolving to a Blob, since browser environments do not support NodeJS.ReadableStream.
5
- */
6
- export async function getBinaryResponseBody(
7
- result: StreamableMethod,
8
- expectedStatuses: string[] = ["200"],
9
- deserializeError?: (error: any) => unknown
10
- ): Promise<{
11
- blobBody?: Promise<Blob>;
12
- readableStreamBody?: NodeJS.ReadableStream;
13
- }> {
14
- const nodeStream = await result.asNodeStream();
15
- if (!expectedStatuses.includes(nodeStream.status)) {
16
- const error = createRestError(nodeStream);
17
- if (deserializeError) {
18
- error.details = deserializeError(nodeStream.body);
19
- }
20
-
21
- throw error;
22
- }
23
- return { blobBody: undefined, readableStreamBody: nodeStream.body };
24
- }