@azure-tools/typespec-ts 0.55.0-dev.9 → 0.55.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-tools/typespec-ts",
3
- "version": "0.55.0-dev.9",
3
+ "version": "0.55.0",
4
4
  "description": "An experimental TypeSpec emitter for TypeScript",
5
5
  "main": "dist/src/index.js",
6
6
  "type": "module",
@@ -18,15 +18,6 @@
18
18
  "license": "MIT",
19
19
  "devDependencies": {
20
20
  "@azure-rest/core-client": "^2.3.1",
21
- "@typespec/http-specs": "^0.1.0-alpha.38 || >=0.1.0-alpha.39-dev <0.1.0-alpha.39",
22
- "@typespec/spector": "^0.1.0-alpha.25 || >=0.1.0-alpha.26-dev <0.1.0-alpha.26",
23
- "@typespec/spec-api": "^0.1.0-alpha.14 || >=0.1.0-alpha.15-dev <0.1.0-alpha.15",
24
- "@typespec/tspd": "^0.75.0 || >=0.76.0-dev <0.76.0",
25
- "@azure-tools/azure-http-specs": "^0.1.0-alpha.42 || >=0.1.0-alpha.43-dev <0.1.0-alpha.43",
26
- "@azure-tools/typespec-autorest": "^0.69.1 || >=0.70.0-dev <0.70.0",
27
- "@azure-tools/typespec-azure-core": "^0.69.0 || >=0.70.0-dev <0.70.0",
28
- "@azure-tools/typespec-azure-resource-manager": "^0.69.1 || >=0.70.0-dev <0.70.0",
29
- "@azure-tools/typespec-client-generator-core": "^0.69.0 || >=0.70.0-dev <0.70.0",
30
21
  "@azure/abort-controller": "^2.1.2",
31
22
  "@azure/core-auth": "^1.6.0",
32
23
  "@azure/core-lro": "^3.1.0",
@@ -34,13 +25,7 @@
34
25
  "@azure/core-rest-pipeline": "^1.14.0",
35
26
  "@azure/core-util": "^1.4.0",
36
27
  "@azure/logger": "^1.0.4",
37
- "@typespec/compiler": "^1.13.0",
38
- "@typespec/http": "^1.13.0",
39
- "@typespec/openapi": "^1.13.0",
40
- "@typespec/rest": "^0.83.0 || >=0.84.0-dev <0.84.0",
41
- "@typespec/versioning": "^0.83.0 || >=0.84.0-dev <0.84.0",
42
- "@typespec/xml": "^0.83.0 || >=0.84.0-dev <0.84.0",
43
- "@typespec/ts-http-runtime": "0.3.5",
28
+ "@typespec/ts-http-runtime": "0.3.6",
44
29
  "cross-env": "^10.1.0",
45
30
  "mkdirp": "^3.0.1",
46
31
  "npm-run-all": "~4.1.5",
@@ -49,17 +34,31 @@
49
34
  "vite": "^8.0.8",
50
35
  "@vitest/coverage-v8": "^4.1.3",
51
36
  "@microsoft/api-extractor": "^7.58.1",
52
- "tsx": "^4.21.0",
53
- "yaml": "^2.8.3"
37
+ "yaml": "^2.8.3",
38
+ "@typespec/http-specs": "^0.1.0-alpha.38",
39
+ "@typespec/spector": "^0.1.0-alpha.25",
40
+ "@typespec/spec-api": "^0.1.0-alpha.14",
41
+ "@azure-tools/azure-http-specs": "^0.1.0-alpha.42",
42
+ "@azure-tools/typespec-autorest": "^0.69.1",
43
+ "@typespec/tspd": "^0.75.0",
44
+ "@azure-tools/typespec-azure-core": "^0.69.0",
45
+ "@azure-tools/typespec-azure-resource-manager": "^0.69.1",
46
+ "@azure-tools/typespec-client-generator-core": "^0.69.1",
47
+ "@typespec/compiler": "^1.13.0",
48
+ "@typespec/openapi": "^1.13.0",
49
+ "@typespec/rest": "^0.83.0",
50
+ "@typespec/versioning": "^0.83.0",
51
+ "@typespec/xml": "^0.83.0",
52
+ "@typespec/http": "^1.13.0"
54
53
  },
55
54
  "peerDependencies": {
56
- "@azure-tools/typespec-azure-core": "^0.69.0 || >=0.70.0-dev <0.70.0",
57
- "@azure-tools/typespec-client-generator-core": "^0.69.0 || >=0.70.0-dev <0.70.0",
58
- "@typespec/compiler": "^1.13.0",
55
+ "@azure-tools/typespec-azure-core": "^0.69.0",
59
56
  "@typespec/http": "^1.13.0",
60
- "@typespec/rest": "^0.83.0 || >=0.84.0-dev <0.84.0",
61
- "@typespec/versioning": "^0.83.0 || >=0.84.0-dev <0.84.0",
62
- "@typespec/xml": "^0.83.0 || >=0.84.0-dev <0.84.0"
57
+ "@typespec/compiler": "^1.13.0",
58
+ "@azure-tools/typespec-client-generator-core": "^0.69.1",
59
+ "@typespec/rest": "^0.83.0",
60
+ "@typespec/versioning": "^0.83.0",
61
+ "@typespec/xml": "^0.83.0"
63
62
  },
64
63
  "dependencies": {
65
64
  "fast-xml-parser": "^5.7.0",
@@ -93,23 +92,26 @@
93
92
  "test:azure": "npm run test-next && npm run copy:typespec && npm run integration-test-ci:azure-modular",
94
93
  "lint": "eslint . --ext .ts --max-warnings=0",
95
94
  "lint:fix": "eslint . --fix --ext .ts",
96
- "check:tree": "tsx ./test/commands/check-clean-tree.ts",
95
+ "check:tree": "node ./test/commands/check-clean-tree.ts",
97
96
  "test:ts:e2e": "npm run copy:typespec && npm run integration-test-ci:azure-modular",
98
97
  "integration-test-ci": "npm-run-all copy:typespec integration-test-ci:azure-modular",
99
98
  "integration-test-ci:sequential": "npm-run-all --serial copy:typespec integration-test-ci:azure-modular",
100
99
  "integration-test-ci:azure-modular": "npm-run-all --silent -p start-test-server:azure-modular --race generate-and-run:azure-modular",
101
100
  "start-test-server": "mkdirp -p coverage && npx tsp-spector serve ./node_modules/@typespec/http-specs/specs --coverageFile ./coverage/spector-coverage-typescript.json",
102
101
  "start-test-server:azure-modular": "mkdirp -p coverage && npx tsp-spector serve ./node_modules/@azure-tools/azure-http-specs/specs ./node_modules/@typespec/http-specs/specs --port 3002 --coverageFile ./coverage/spector-coverage-typescript-modular-azure.json",
103
- "copy:typespec": "rm -rf temp && mkdirp -p temp/specs && mkdirp -p temp/assets && cp -r ./node_modules/@typespec/http-specs/specs/* ./temp/specs && cp -r ./node_modules/@azure-tools/azure-http-specs/specs/* ./temp/specs && cp -r ./test/integration/typespec/* ./temp/specs && cp -r ./node_modules/@typespec/http-specs/assets/* ./temp/assets",
104
- "generate-and-run:azure-modular": "npm run generate-tsp-only:azure-modular && npm run integration-test:alone:azure-modular && npm run stop-test-server -- -p 3002",
102
+ "copy:typespec": "node ./test/commands/copy-typespec.ts",
103
+ "generate-and-run:azure-modular": "npm run generate-tsp-only:azure-modular:client && npm-run-all -p integration-test:alone:azure-modular generate-tsp-only:azure-modular:declarations && npm run stop-test-server -- -p 3002",
105
104
  "generate-tsp-only": "npm run generate-tsp-only:azure-modular",
106
- "generate-tsp-only:azure-modular": "npx tsx ./test/commands/gen-spector.js --tag=azure-modular",
105
+ "generate-tsp-only:azure-modular": "node ./test/commands/gen-spector.js",
106
+ "generate-tsp-only:azure-modular:client": "node ./test/commands/gen-spector.js --phase=client",
107
+ "generate-tsp-only:azure-modular:declarations": "node ./test/commands/gen-spector.js --phase=declarations",
107
108
  "regen-test-baselines": "npm run generate-tsp-only",
108
109
  "integration-test:alone": "npm run integration-test:alone:azure-modular",
109
110
  "integration-test:alone:azure-modular": "vitest run --project integration-azure-modular",
110
111
  "stop-test-server": "npx tsp-spector server stop",
111
112
  "unit-test": "npm run unit-test:modular",
112
- "unit-test:modular": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vitest run --project unit-modular",
113
+ "unit-test:modular": "cross-env NODE_OPTIONS=--max-old-space-size=1024 vitest run --project unit-modular",
114
+ "gen:scenario-suites": "node ./test/commands/gen-scenario-suites.ts",
113
115
  "regen-docs": "npm run build && tspd doc . --enable-experimental --output-dir ../../website/src/content/docs/docs/emitters/clients/typespec-ts/reference --skip-js"
114
116
  }
115
117
  }
@@ -23,10 +23,11 @@ export const flattenPropertyModelMap: Map<SdkModelPropertyType, SdkModelType> =
23
23
  SdkModelType
24
24
  >();
25
25
  /**
26
- * Set of paged result models that are also used in non-paging operations.
26
+ * Set of paged result models that should keep their public name (no "_" prefix).
27
+ * This includes models used as non-paging method responses or as model properties.
27
28
  * Precomputed during visitPackageTypes for O(1) lookups in normalizeModelName.
28
29
  */
29
- export const pagedModelsUsedInNonPagingOps = new Set<SdkType>();
30
+ export const pagedModelsKeptPublic = new Set<SdkType>();
30
31
 
31
32
  export interface SdkTypeContext {
32
33
  operations: Map<Type, SdkServiceMethod<SdkHttpOperation>>;
@@ -290,8 +290,8 @@ function buildClientOperationGroups(
290
290
  } else {
291
291
  // The `rawGroupName` is used to any places where we need normalized name twice so we need to keep the raw as PascalCase.
292
292
  const rawGroupName = normalizeName(prefixes[0] ?? "", NameType.Interface);
293
- const operationName = `_get${normalizeName(rawGroupName, NameType.OperationGroup)}Operations`;
294
- const propertyType = `${normalizeName(rawGroupName, NameType.OperationGroup)}Operations`;
293
+ const operationName = `_get${rawGroupName}Operations`;
294
+ const propertyType = `${rawGroupName}Operations`;
295
295
  // The `groupName` is used to any places where we don't need normalized name again
296
296
  const groupName = normalizeName(rawGroupName, NameType.Property);
297
297
  const existProperty = clientClass.getProperties().filter((p) => {
@@ -44,7 +44,7 @@ import {
44
44
  emitQueue,
45
45
  flattenPropertyModelMap,
46
46
  getAllOperationsFromClient,
47
- pagedModelsUsedInNonPagingOps,
47
+ pagedModelsKeptPublic,
48
48
  } from "../framework/hooks/sdk-types.js";
49
49
  import { refkey } from "../framework/refkey.js";
50
50
  import { reportDiagnostic } from "../lib.js";
@@ -841,8 +841,7 @@ export function normalizeModelName(
841
841
  }
842
842
  const namespacePrefix = context.rlcOptions?.enableModelNamespace ? segments.join("") : "";
843
843
  const internalModelPrefix =
844
- (isPagedResultModel(context, type) && !pagedModelsUsedInNonPagingOps.has(type)) ||
845
- type.isGeneratedName
844
+ (isPagedResultModel(context, type) && !pagedModelsKeptPublic.has(type)) || type.isGeneratedName
846
845
  ? "_"
847
846
  : "";
848
847
  return `${internalModelPrefix}${normalizeName(namespacePrefix + type.name, nameType, true)}${unionSuffix}`;
@@ -932,7 +931,7 @@ export function visitPackageTypes(context: SdkContext) {
932
931
  const { sdkPackage } = context;
933
932
  emitQueue.clear();
934
933
  flattenPropertyModelMap.clear();
935
- pagedModelsUsedInNonPagingOps.clear();
934
+ pagedModelsKeptPublic.clear();
936
935
  // Add all models in the package to the emit queue
937
936
  for (const model of sdkPackage.models) {
938
937
  visitType(context, model);
@@ -1015,7 +1014,19 @@ function trackPagedModelInNonPagingMethod(
1015
1014
  if (method.kind !== "basic" && method.kind !== "lro") return;
1016
1015
  const respType = method.response.type;
1017
1016
  if (respType && isPagedResultModel(context, respType)) {
1018
- pagedModelsUsedInNonPagingOps.add(respType);
1017
+ pagedModelsKeptPublic.add(respType);
1018
+ }
1019
+ }
1020
+
1021
+ /**
1022
+ * If a paged result model is used as a model property type, mark it so that
1023
+ * normalizeModelName keeps it public (no "_" prefix).
1024
+ */
1025
+ function trackPagedModelUsedAsProperty(context: SdkContext, type: SdkType): void {
1026
+ // Unwrap array types to check the element type
1027
+ const resolved = type.kind === "array" ? type.valueType : type;
1028
+ if (resolved && isPagedResultModel(context, resolved)) {
1029
+ pagedModelsKeptPublic.add(resolved);
1019
1030
  }
1020
1031
  }
1021
1032
 
@@ -1044,6 +1055,10 @@ function visitType(context: SdkContext, type: SdkType | undefined) {
1044
1055
  if (property.flatten && property.type.kind === "model") {
1045
1056
  flattenPropertyModelMap.set(property, type);
1046
1057
  }
1058
+ // If a paged result model is used as a model property (not as a paging
1059
+ // operation response), it must stay public (no "_" prefix) because users
1060
+ // interact with it directly through the property.
1061
+ trackPagedModelUsedAsProperty(context, property.type);
1047
1062
  }
1048
1063
  if (type.discriminatedSubtypes) {
1049
1064
  for (const subType of Object.values(type.discriminatedSubtypes)) {
@@ -316,12 +316,36 @@ function prepareExampleParameters(
316
316
 
317
317
  let subscriptionIdValue = `"00000000-0000-0000-0000-000000000000"`;
318
318
  let isSubscriptionIdAdded = false;
319
- // required parameters
319
+
320
+ // Map each HTTP parameter to its method parameter via `methodParameterSegments`
321
+ // and place its example value at that path, yielding one argument per method
322
+ // parameter regardless of flat, nested, or grouped shape. Client- and
323
+ // body-carried roots are emitted by their own sections and skipped here.
324
+ const bodyParam = method.operation.bodyParam;
325
+ let bodyRootName: string | undefined;
326
+ if (
327
+ bodyParam &&
328
+ !isSpreadBodyParameter(bodyParam) &&
329
+ bodyParam.methodParameterSegments.length === 1 &&
330
+ bodyParam.methodParameterSegments[0] !== undefined &&
331
+ bodyParam.methodParameterSegments[0].length >= 1
332
+ ) {
333
+ bodyRootName = bodyParam.methodParameterSegments[0][0]!.name;
334
+ }
335
+
336
+ const methodArguments = new Map<
337
+ string,
338
+ { name: string; isOptional: boolean; value: ValueTreeNode | string }
339
+ >();
320
340
  for (const param of method.operation.parameters) {
321
- if (param.optional === true || param.type.kind === "constant" || param.clientDefaultValue) {
341
+ if (param.type.kind === "constant" || param.clientDefaultValue) {
322
342
  continue;
323
343
  }
324
-
344
+ const path = param.methodParameterSegments[0];
345
+ if (path === undefined || path.length === 0) {
346
+ continue;
347
+ }
348
+ const root = path[0]!;
325
349
  const exampleValue = parameterMap[param.serializedName];
326
350
 
327
351
  // Handle subscriptionId parameter separately for ARM clients
@@ -343,28 +367,27 @@ function prepareExampleParameters(
343
367
  continue;
344
368
  }
345
369
 
370
+ // Roots emitted by the client and body sections.
371
+ if (root.onClient || (bodyRootName !== undefined && root.name === bodyRootName)) {
372
+ continue;
373
+ }
374
+
346
375
  if (!exampleValue || !exampleValue.value) {
347
376
  // report diagnostic if required parameter is missing
348
- reportDiagnostic(dpgContext.program, {
349
- code: "required-sample-parameter",
350
- format: {
351
- exampleName: method.oriName ?? method.name,
352
- paramName: param.name,
353
- },
354
- target: NoTarget,
355
- });
377
+ if (path.length === 1 && !param.optional) {
378
+ reportDiagnostic(dpgContext.program, {
379
+ code: "required-sample-parameter",
380
+ format: {
381
+ exampleName: method.oriName ?? method.name,
382
+ paramName: param.name,
383
+ },
384
+ target: NoTarget,
385
+ });
386
+ }
356
387
  continue;
357
388
  }
358
389
 
359
- result.push(
360
- prepareExampleValue(
361
- dpgContext,
362
- exampleValue.parameter.name,
363
- exampleValue.value,
364
- param.optional,
365
- param.onClient,
366
- ),
367
- );
390
+ placeMethodArgument(methodArguments, path, getParameterValue(dpgContext, exampleValue.value));
368
391
  }
369
392
 
370
393
  // If client-level subscriptionId is needed on the client for this method, then add it
@@ -381,7 +404,6 @@ function prepareExampleParameters(
381
404
  }
382
405
 
383
406
  // required/optional body parameters
384
- const bodyParam = method.operation.bodyParam;
385
407
  const bodySerializeName = bodyParam?.serializedName;
386
408
  const bodyExample = parameterMap[bodySerializeName ?? ""];
387
409
  if (bodyParam && bodyExample && bodyExample.value) {
@@ -441,28 +463,87 @@ function prepareExampleParameters(
441
463
  }
442
464
  }
443
465
  }
444
- // optional parameters
445
- method.operation.parameters
446
- .filter(
447
- (param) =>
448
- param.optional === true && parameterMap[param.serializedName] && !param.clientDefaultValue,
449
- )
450
- .map((param) => parameterMap[param.serializedName]!)
451
- .forEach((param) => {
452
- result.push(
453
- prepareExampleValue(
454
- dpgContext,
455
- param.parameter.name,
456
- param.value,
457
- true,
458
- param.parameter.onClient,
459
- ),
460
- );
466
+ // Emit one argument per top-level method parameter. A normal parameter carries
467
+ // a scalar value; a parameter with nested leaves carries an object literal.
468
+ for (const arg of methodArguments.values()) {
469
+ result.push({
470
+ name: normalizeName(arg.name, NameType.Parameter, true),
471
+ value: serializeNestedValue(arg.value),
472
+ isOptional: arg.isOptional,
473
+ onClient: false,
461
474
  });
475
+ }
462
476
 
463
477
  return result;
464
478
  }
465
479
 
480
+ /** A nested value tree: leaves are TypeScript value expressions, interior nodes are objects. */
481
+ interface ValueTreeNode {
482
+ [key: string]: ValueTreeNode | string;
483
+ }
484
+
485
+ /**
486
+ * Records `leaf` for the method parameter addressed by `path`. `path[0]` is the
487
+ * top-level method parameter (the argument key); a single-segment path stores a
488
+ * scalar, a longer path stores the leaf inside a nested object merged with
489
+ * sibling leaves sharing the same root.
490
+ */
491
+ function placeMethodArgument(
492
+ methodArguments: Map<
493
+ string,
494
+ { name: string; isOptional: boolean; value: ValueTreeNode | string }
495
+ >,
496
+ path: readonly { name: string; optional?: boolean }[],
497
+ leaf: string,
498
+ ): void {
499
+ const root = path[0]!;
500
+ let arg = methodArguments.get(root.name);
501
+ if (!arg) {
502
+ arg = { name: root.name, isOptional: Boolean(root.optional), value: {} };
503
+ methodArguments.set(root.name, arg);
504
+ }
505
+ if (path.length === 1) {
506
+ arg.value = leaf;
507
+ return;
508
+ }
509
+ if (typeof arg.value !== "object") {
510
+ arg.value = {};
511
+ }
512
+ setNestedValue(arg.value, path.slice(1), leaf);
513
+ }
514
+
515
+ /** Places `leaf` at `path` (segments after the root), creating intermediate objects as needed. */
516
+ function setNestedValue(
517
+ root: ValueTreeNode,
518
+ path: readonly { name: string }[],
519
+ leaf: string,
520
+ ): void {
521
+ let node = root;
522
+ for (let i = 0; i < path.length; i++) {
523
+ const key = normalizeName(path[i]!.name, NameType.Property, true);
524
+ if (i === path.length - 1) {
525
+ node[key] = leaf;
526
+ } else {
527
+ const existing = node[key];
528
+ if (typeof existing !== "object" || existing === null) {
529
+ node[key] = {};
530
+ }
531
+ node = node[key] as ValueTreeNode;
532
+ }
533
+ }
534
+ }
535
+
536
+ /** Serializes a {@link ValueTreeNode} into a TypeScript object literal string. */
537
+ function serializeNestedValue(node: ValueTreeNode | string): string {
538
+ if (typeof node === "string") {
539
+ return node;
540
+ }
541
+ const entries = Object.entries(node).map(
542
+ ([key, value]) => `${key}: ${serializeNestedValue(value)}`,
543
+ );
544
+ return `{ ${entries.join(", ")} }`;
545
+ }
546
+
466
547
  function getCredentialExampleValue(
467
548
  _dpgContext: SdkContext,
468
549
  initialization: SdkClientInitializationType,
@@ -61,7 +61,7 @@ export function isSupportedSerializeType(type: SdkType): boolean {
61
61
  * 6. nested model i.e. model with property that is a model with one of the above three conditions.
62
62
  * If we consider array type, with all the above 6 types as the element types.
63
63
  */
64
- const specialVariantMap = new Map<SdkType, boolean>();
64
+ const specialVariantMap = new WeakMap<SdkType, boolean>();
65
65
  export function isSpecialUnionVariant(
66
66
  t: SdkType & { isNonExhaustive?: boolean },
67
67
  variantStack: SdkType[] = [],
@@ -146,7 +146,7 @@ export function isSpecialHandledUnion(t: SdkType & { isNonExhaustive?: boolean }
146
146
  return isDiscriminatedUnion(t!) || isPolymorphicUnion(t);
147
147
  }
148
148
 
149
- const polymorphicUnionMap = new Map<SdkType, boolean>();
149
+ const polymorphicUnionMap = new WeakMap<SdkType, boolean>();
150
150
  export function isPolymorphicUnion(t: SdkType): boolean {
151
151
  if (polymorphicUnionMap.has(t)) {
152
152
  return polymorphicUnionMap.get(t) ?? false;