@azure-tools/typespec-ts 0.56.0-dev.3 → 0.56.0-dev.6

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 (63) hide show
  1. package/README.md +0 -6
  2. package/dist/src/framework/declaration.d.ts.map +1 -1
  3. package/dist/src/framework/declaration.js +6 -20
  4. package/dist/src/framework/declaration.js.map +1 -1
  5. package/dist/src/framework/source-file-batch.d.ts +35 -0
  6. package/dist/src/framework/source-file-batch.d.ts.map +1 -0
  7. package/dist/src/framework/source-file-batch.js +128 -0
  8. package/dist/src/framework/source-file-batch.js.map +1 -0
  9. package/dist/src/interfaces.d.ts +0 -1
  10. package/dist/src/interfaces.d.ts.map +1 -1
  11. package/dist/src/interfaces.js.map +1 -1
  12. package/dist/src/lib.d.ts +0 -1
  13. package/dist/src/lib.d.ts.map +1 -1
  14. package/dist/src/lib.js +0 -5
  15. package/dist/src/lib.js.map +1 -1
  16. package/dist/src/metadata/build-readme-file.d.ts.map +1 -1
  17. package/dist/src/metadata/build-readme-file.js +14 -67
  18. package/dist/src/metadata/build-readme-file.js.map +1 -1
  19. package/dist/src/metadata/render-template.d.ts +24 -0
  20. package/dist/src/metadata/render-template.d.ts.map +1 -0
  21. package/dist/src/metadata/render-template.js +148 -0
  22. package/dist/src/metadata/render-template.js.map +1 -0
  23. package/dist/src/metadata/test/build-recorded-client.d.ts.map +1 -1
  24. package/dist/src/metadata/test/build-recorded-client.js +1 -7
  25. package/dist/src/metadata/test/build-recorded-client.js.map +1 -1
  26. package/dist/src/metadata/test/build-sample-test.d.ts.map +1 -1
  27. package/dist/src/metadata/test/build-sample-test.js +1 -4
  28. package/dist/src/metadata/test/build-sample-test.js.map +1 -1
  29. package/dist/src/metadata/test/build-snippets.d.ts.map +1 -1
  30. package/dist/src/metadata/test/build-snippets.js +2 -4
  31. package/dist/src/metadata/test/build-snippets.js.map +1 -1
  32. package/dist/src/metadata/test/template.d.ts +0 -1
  33. package/dist/src/metadata/test/template.d.ts.map +1 -1
  34. package/dist/src/metadata/test/template.js +0 -127
  35. package/dist/src/metadata/test/template.js.map +1 -1
  36. package/dist/src/modular/build-root-index.d.ts.map +1 -1
  37. package/dist/src/modular/build-root-index.js +37 -9
  38. package/dist/src/modular/build-root-index.js.map +1 -1
  39. package/dist/src/modular/build-subpath-index.d.ts.map +1 -1
  40. package/dist/src/modular/build-subpath-index.js +15 -2
  41. package/dist/src/modular/build-subpath-index.js.map +1 -1
  42. package/dist/src/modular/emit-models.d.ts.map +1 -1
  43. package/dist/src/modular/emit-models.js +24 -17
  44. package/dist/src/modular/emit-models.js.map +1 -1
  45. package/dist/src/transform/transform-client-options.d.ts.map +1 -1
  46. package/dist/src/transform/transform-client-options.js +0 -2
  47. package/dist/src/transform/transform-client-options.js.map +1 -1
  48. package/dist/tsconfig.tsbuildinfo +1 -1
  49. package/package.json +6 -9
  50. package/src/framework/declaration.ts +8 -20
  51. package/src/framework/source-file-batch.ts +140 -0
  52. package/src/interfaces.ts +0 -1
  53. package/src/lib.ts +0 -6
  54. package/src/metadata/build-readme-file.ts +17 -91
  55. package/src/metadata/render-template.ts +202 -0
  56. package/src/metadata/test/build-recorded-client.ts +1 -7
  57. package/src/metadata/test/build-sample-test.ts +1 -4
  58. package/src/metadata/test/build-snippets.ts +2 -4
  59. package/src/metadata/test/template.ts +0 -128
  60. package/src/modular/build-root-index.ts +52 -14
  61. package/src/modular/build-subpath-index.ts +24 -3
  62. package/src/modular/emit-models.ts +24 -18
  63. package/src/transform/transform-client-options.ts +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-tools/typespec-ts",
3
- "version": "0.56.0-dev.3",
3
+ "version": "0.56.0-dev.6",
4
4
  "description": "A TypeSpec emitter for TypeScript",
5
5
  "main": "dist/src/index.js",
6
6
  "type": "module",
@@ -42,9 +42,7 @@
42
42
  "@typespec/xml": "^0.83.0 || >=0.84.0-dev <0.84.0",
43
43
  "@typespec/ts-http-runtime": "0.3.6",
44
44
  "cross-env": "^10.1.0",
45
- "mkdirp": "^3.0.1",
46
- "npm-run-all": "~4.1.5",
47
- "prettier": "^3.8.1",
45
+ "concurrently": "^10.0.0",
48
46
  "vitest": "^4.1.3",
49
47
  "vite": "^8.0.8",
50
48
  "@vitest/coverage-v8": "^4.1.3",
@@ -62,7 +60,6 @@
62
60
  },
63
61
  "dependencies": {
64
62
  "fast-xml-parser": "^5.7.0",
65
- "handlebars": "^4.7.7",
66
63
  "prettier": "^3.8.1",
67
64
  "ts-morph": "^23.0.0",
68
65
  "tslib": "^2.3.1"
@@ -92,11 +89,11 @@
92
89
  "lint": "eslint . --ext .ts --max-warnings=0",
93
90
  "lint:fix": "eslint . --fix --ext .ts",
94
91
  "check:tree": "node ./test/commands/check-clean-tree.ts",
95
- "integration-test-ci": "npm-run-all copy:typespec spector-test",
96
- "spector-test": "npm-run-all --silent -p start-test-server --race generate-and-run",
97
- "start-test-server": "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-azure.json",
92
+ "integration-test-ci": "npm run copy:typespec && npm run spector-test",
93
+ "spector-test": "concurrently --kill-others --success first \"npm:start-test-server\" \"npm:generate-and-run\"",
94
+ "start-test-server": "node ./test/commands/start-test-server.js",
98
95
  "copy:typespec": "node ./test/commands/copy-typespec.ts",
99
- "generate-and-run": "npm run generate-tsp-only:client && npm-run-all -p integration-test:alone generate-tsp-only:declarations && npm run stop-test-server -- -p 3002",
96
+ "generate-and-run": "npm run generate-tsp-only:client && concurrently --kill-others-on-fail \"npm:integration-test:alone\" \"npm:generate-tsp-only:declarations\" && npm run stop-test-server -- -p 3002",
100
97
  "generate-tsp-only": "node ./test/commands/gen-spector.js",
101
98
  "generate-tsp-only:client": "node ./test/commands/gen-spector.js --phase=client",
102
99
  "generate-tsp-only:declarations": "node ./test/commands/gen-spector.js --phase=declarations",
@@ -8,12 +8,14 @@ import {
8
8
  InterfaceDeclaration,
9
9
  InterfaceDeclarationStructure,
10
10
  SourceFile,
11
+ StatementStructures,
11
12
  StructureKind,
12
13
  TypeAliasDeclaration,
13
14
  TypeAliasDeclarationStructure,
14
15
  } from "ts-morph";
15
16
  import { useBinder } from "./hooks/binder.js";
16
17
  import { refkey as getRefKey } from "./refkey.js";
18
+ import { enqueueStatement } from "./source-file-batch.js";
17
19
  export type DeclarationStructures =
18
20
  | ClassDeclarationStructure
19
21
  | EnumDeclarationStructure
@@ -80,25 +82,11 @@ export function addDeclaration(
80
82
  // Update the declaration name to be unique
81
83
  const trackedDeclaration = { ...declaration, name: trackedDeclarationName };
82
84
 
83
- switch (trackedDeclaration.kind) {
84
- case StructureKind.Class:
85
- sourceFile.addClass(trackedDeclaration);
86
- break;
87
- case StructureKind.Enum:
88
- sourceFile.addEnum(trackedDeclaration);
89
- break;
90
- case StructureKind.Function:
91
- sourceFile.addFunction(trackedDeclaration);
92
- break;
93
- case StructureKind.Interface:
94
- sourceFile.addInterface(trackedDeclaration);
95
- break;
96
- case StructureKind.TypeAlias:
97
- if (trackedDeclaration.type) {
98
- sourceFile.addTypeAlias(trackedDeclaration);
99
- }
100
- break;
101
- default:
102
- throw new Error(`Unsupported declaration kind ${(trackedDeclaration as any).kind}`);
85
+ // Skip empty type aliases (they have no body to emit). Done before
86
+ // dispatching so behaviour is identical whether batching or not.
87
+ if (trackedDeclaration.kind === StructureKind.TypeAlias && !trackedDeclaration.type) {
88
+ return;
103
89
  }
90
+
91
+ enqueueStatement(sourceFile, trackedDeclaration as StatementStructures);
104
92
  }
@@ -0,0 +1,140 @@
1
+ import {
2
+ ExportDeclarationStructure,
3
+ SourceFile,
4
+ StatementStructures,
5
+ StructureKind,
6
+ } from "ts-morph";
7
+
8
+ /**
9
+ * Generic per-source-file batch for ts-morph `add*` mutations.
10
+ *
11
+ * Each individual `sourceFile.addInterface/addFunction/addExportDeclaration/...`
12
+ * call re-parses the entire source file in ts-morph. Calling them in a loop
13
+ * therefore grows as O(N × file_size). This module collects structures while
14
+ * a batch is open and, on flush, issues a single `addStatements` call per file,
15
+ * collapsing N re-parses into 1.
16
+ *
17
+ * The batch is reference-counted so nested begin/flush pairs compose safely.
18
+ * Statements flush in insertion order and produce byte-identical output.
19
+ */
20
+
21
+ let batchDepth = 0;
22
+ const pendingByFile = new Map<SourceFile, StatementStructures[]>();
23
+
24
+ /**
25
+ * Opens a batching scope. Subsequent `enqueueStatement` calls queue rather
26
+ * than mutate the AST until the matching `flushSourceFileBatch` is called.
27
+ */
28
+ export function beginSourceFileBatch(): void {
29
+ batchDepth++;
30
+ }
31
+
32
+ /**
33
+ * Closes the most recent batching scope. When the outermost scope closes,
34
+ * all pending structures are written to their source files in bulk.
35
+ */
36
+ export function flushSourceFileBatch(): void {
37
+ if (batchDepth === 0) {
38
+ return;
39
+ }
40
+ batchDepth--;
41
+ if (batchDepth > 0) {
42
+ return;
43
+ }
44
+ try {
45
+ for (const [sourceFile, statements] of pendingByFile) {
46
+ if (statements.length === 0) {
47
+ continue;
48
+ }
49
+ sourceFile.addStatements(statements);
50
+ }
51
+ } finally {
52
+ pendingByFile.clear();
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Queues a statement structure for bulk-add on flush, or writes it
58
+ * immediately when no batch is open.
59
+ * @param sourceFile - Target source file.
60
+ * @param structure - The statement structure to add.
61
+ */
62
+ export function enqueueStatement(sourceFile: SourceFile, structure: StatementStructures): void {
63
+ if (batchDepth > 0) {
64
+ let pending = pendingByFile.get(sourceFile);
65
+ if (!pending) {
66
+ pending = [];
67
+ pendingByFile.set(sourceFile, pending);
68
+ }
69
+ pending.push(structure);
70
+ return;
71
+ }
72
+ sourceFile.addStatements([structure]);
73
+ }
74
+
75
+ /**
76
+ * Returns the set of names exported from `sourceFile` considering both the
77
+ * declarations already present in its AST and any export declarations queued
78
+ * by the current batch. Use this in place of reading `getExportDeclarations`
79
+ * directly when callers dedup against existing exports inside a batch.
80
+ * @param sourceFile - The file to inspect.
81
+ */
82
+ export function getEffectiveExportedNames(sourceFile: SourceFile): Set<string> {
83
+ const names = new Set<string>();
84
+ for (const decl of sourceFile.getExportDeclarations()) {
85
+ for (const named of decl.getNamedExports()) {
86
+ names.add(named.getAliasNode()?.getText() ?? named.getName());
87
+ }
88
+ }
89
+ const pending = pendingByFile.get(sourceFile);
90
+ if (pending) {
91
+ for (const structure of pending) {
92
+ if (structure.kind !== StructureKind.ExportDeclaration) {
93
+ continue;
94
+ }
95
+ collectNamedExportNames(structure.namedExports, names);
96
+ }
97
+ }
98
+ return names;
99
+ }
100
+
101
+ /**
102
+ * Returns the set of named exports queued in the current batch for
103
+ * `sourceFile`. Use this to union with any AST-based view of exports
104
+ * (e.g. `getExportedDeclarations`) when deduplicating against still-pending
105
+ * writes inside a batch.
106
+ * @param sourceFile - The file to inspect.
107
+ */
108
+ export function getQueuedExportNames(sourceFile: SourceFile): Set<string> {
109
+ const names = new Set<string>();
110
+ const pending = pendingByFile.get(sourceFile);
111
+ if (!pending) {
112
+ return names;
113
+ }
114
+ for (const structure of pending) {
115
+ if (structure.kind !== StructureKind.ExportDeclaration) {
116
+ continue;
117
+ }
118
+ collectNamedExportNames(structure.namedExports, names);
119
+ }
120
+ return names;
121
+ }
122
+
123
+ function collectNamedExportNames(
124
+ named: ExportDeclarationStructure["namedExports"],
125
+ into: Set<string>,
126
+ ): void {
127
+ if (!named || typeof named === "function") {
128
+ // WriterFunction form is opaque to us; callers can't dedup by name.
129
+ return;
130
+ }
131
+ for (const item of named) {
132
+ if (typeof item === "string") {
133
+ into.add(item);
134
+ } else if (typeof item === "function") {
135
+ continue;
136
+ } else if (item) {
137
+ into.add(item.alias ?? item.name);
138
+ }
139
+ }
140
+ }
package/src/interfaces.ts CHANGED
@@ -223,7 +223,6 @@ export interface ClientOptions {
223
223
  generateTest?: boolean;
224
224
  generateSample?: boolean;
225
225
  azureOutputDirectory?: string;
226
- isTypeSpecTest?: boolean;
227
226
  serviceInfo?: ServiceInfo;
228
227
  azureArm?: boolean;
229
228
  enableOperationGroup?: boolean;
package/src/lib.ts CHANGED
@@ -39,7 +39,6 @@ export interface EmitterOptions {
39
39
  */
40
40
  "generate-test"?: boolean;
41
41
  "generate-sample"?: boolean;
42
- "is-typespec-test"?: boolean;
43
42
  "azure-arm"?: boolean;
44
43
  "enable-operation-group"?: boolean;
45
44
  "enable-model-namespace"?: boolean;
@@ -170,11 +169,6 @@ export const EmitterOptionsSchema: JSONSchemaType<EmitterOptions> = {
170
169
  description:
171
170
  "Whether to generate sample files, for basic samples of your generated sdks. Defaults to `undefined`. Management packages' default to `true`.",
172
171
  },
173
- "is-typespec-test": {
174
- type: "boolean",
175
- nullable: true,
176
- description: "Internal option for test",
177
- },
178
172
  "azure-arm": {
179
173
  type: "boolean",
180
174
  nullable: true,
@@ -1,12 +1,10 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
3
 
4
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
5
- // @ts-ignore: to fix the handlebars issue
6
- import hbs from "handlebars";
7
4
  import { ClientModel } from "../interfaces.js";
8
5
  import { getClientName } from "../utils/name-constructors.js";
9
6
  import { NameType, normalizeName } from "../utils/name-utils.js";
7
+ import { renderTemplate } from "./render-template.js";
10
8
 
11
9
  const azureReadmeModularTemplate = `# {{ clientDescriptiveName }} library for JavaScript
12
10
 
@@ -38,13 +36,10 @@ Key links:
38
36
 
39
37
  See our [support policy](https://github.com/Azure/azure-sdk-for-js/blob/main/SUPPORT.md) for more details.
40
38
 
41
- {{#if azure}}
42
39
  ### Prerequisites
43
40
 
44
41
  - An [Azure subscription][azure_sub].
45
- {{/if}}
46
42
 
47
- {{#if isReleasablePackage}}
48
43
  ### Install the \`{{ clientPackageName }}\` package
49
44
 
50
45
  Install the {{ clientDescriptiveName }} library for JavaScript with \`npm\`:
@@ -52,9 +47,7 @@ Install the {{ clientDescriptiveName }} library for JavaScript with \`npm\`:
52
47
  \`\`\`bash
53
48
  npm install {{ clientPackageName }}
54
49
  \`\`\`
55
- {{/if}}
56
50
 
57
- {{#if azure}}
58
51
  {{#if addCredentials}}
59
52
  ### Create and authenticate a \`{{ clientClassName}}\`
60
53
 
@@ -129,7 +122,8 @@ const credential = new InteractiveBrowserCredential({
129
122
  const client = new {{ clientClassName }}("<endpoint>", credential);
130
123
  \`\`\`
131
124
  {{/if}}
132
- {{/if}}{{/if}}
125
+ {{/if}}
126
+
133
127
 
134
128
  ### JavaScript Bundle
135
129
  To use this client library in the browser, first you need to use a bundler. For details on how to do this, please refer to our [bundling documentation](https://aka.ms/AzureSDKBundling).
@@ -140,7 +134,6 @@ To use this client library in the browser, first you need to use a bundler. For
140
134
 
141
135
  \`{{ clientClassName }}\` is the primary interface for developers using the {{ clientDescriptiveName }} library. Explore the methods on this client object to understand the different features of the {{ serviceName }} service that you can access.
142
136
 
143
- {{#if azure}}
144
137
  ## Troubleshooting
145
138
 
146
139
  ### Logging
@@ -153,7 +146,7 @@ import { setLogLevel } from "@azure/logger";
153
146
  setLogLevel("info");
154
147
  \`\`\`
155
148
 
156
- For more detailed instructions on how to enable logs, you can look at the [@azure/logger package docs]({{ repoURL }}/tree/main/sdk/core/logger).
149
+ For more detailed instructions on how to enable logs, you can look at the [@azure/logger package docs](https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/core/logger).
157
150
 
158
151
  {{#if samplesURL}}
159
152
  ## Next steps
@@ -163,54 +156,16 @@ Please take a look at the [samples]({{ samplesURL }}) directory for detailed exa
163
156
 
164
157
  ## Contributing
165
158
 
166
- If you'd like to contribute to this library, please read the [contributing guide]({{ contributingGuideURL }}) to learn more about how to build and test the code.
159
+ If you'd like to contribute to this library, please read the [contributing guide](https://github.com/Azure/azure-sdk-for-js/blob/main/CONTRIBUTING.md) to learn more about how to build and test the code.
167
160
 
168
161
  ## Related projects
169
162
 
170
- - [{{ projectName }}]({{ repoURL }})
163
+ - [Microsoft Azure SDK for JavaScript](https://github.com/Azure/azure-sdk-for-js)
171
164
 
172
165
  [azure_sub]: https://azure.microsoft.com/free/
173
166
  [azure_portal]: https://portal.azure.com
174
- {{#if identityPackageURL}}[azure_identity]: {{ identityPackageURL }}
175
- {{/if}}[defaultazurecredential]: {{ identityPackageURL }}#defaultazurecredential
176
- {{/if}}
177
- `;
178
-
179
- const nonBrandedReadmeTemplate = `# {{ clientDescriptiveName }} library for JavaScript
180
-
181
- {{ description }}
182
-
183
- Key links:
184
-
185
- {{#if packageSourceURL}}
186
- - [Source code]({{ packageSourceURL }})
187
- {{/if}}
188
- {{#if packageNPMURL}}
189
- - [Package (NPM)]({{ packageNPMURL }})
190
- {{/if}}
191
- {{#if apiRefURL}}
192
- - [API reference documentation]({{ apiRefURL }})
193
- {{/if}}
194
- {{#if serviceDocURL}}
195
- - [Product documentation]({{ serviceDocURL }})
196
- {{/if}}
197
- {{#if samplesURL}}
198
- - [Samples]({{ samplesURL }})
199
- {{/if}}
200
-
201
- ## Getting started
202
-
203
- ### Currently supported environments
204
-
205
- - LTS versions of Node.js
206
-
207
- ### Install the \`{{ clientPackageName }}\` package
208
-
209
- Install the {{ clientDescriptiveName }} library for JavaScript with \`npm\`:
210
-
211
- \`\`\`bash
212
- npm install {{ clientPackageName }}
213
- \`\`\`
167
+ [azure_identity]: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/identity/identity
168
+ [defaultazurecredential]: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/identity/identity#defaultazurecredential
214
169
  `;
215
170
 
216
171
  const apiReferenceTemplate = `{{#if apiRefURL}}
@@ -228,8 +183,6 @@ interface Metadata {
228
183
  clientPackageName: string;
229
184
  /** The name of the client class */
230
185
  clientClassName: string;
231
- /** The URL of the repository the package lives in */
232
- repoURL?: string;
233
186
  /** The URL to the package directory in the repository */
234
187
  packageSourceURL?: string;
235
188
  /** The URL to the package's samples */
@@ -240,29 +193,12 @@ interface Metadata {
240
193
  description?: string;
241
194
  /** The URL to the package on npmjs.org */
242
195
  packageNPMURL?: string;
243
- /** The name of the project that lives in the repository */
244
- projectName?: string;
245
196
  /** whether the client accepts standard credentials */
246
197
  addCredentials?: boolean;
247
- /** The link to the identity package in the repository */
248
- identityPackageURL?: string;
249
- /** The URL for the service document */
250
- serviceDocURL?: string;
251
- /** The dependency info for this service */
252
- dependencyDescription?: string;
253
- dependencyLink?: string;
254
- /** Indicates if the package is a multi-client */
255
- hasMultiClients?: boolean;
256
198
  /** The URL to the API reference */
257
199
  apiRefURL?: string;
258
200
  /** Check if the rp is management plane */
259
201
  azureArm?: boolean;
260
- /** Whether the package being generated is for an Azure service */
261
- azure: boolean;
262
- /** Indicates if the package is a test/releasable package. */
263
- isReleasablePackage?: boolean;
264
- /** The link to the contributing guide in the repository */
265
- contributingGuideURL?: string;
266
202
  /** Indicates if the package need generate test files */
267
203
  generateTest?: boolean;
268
204
  /** Indicates if the package need SubscriptionId as the client parameter */
@@ -271,13 +207,10 @@ interface Metadata {
271
207
 
272
208
  export function buildReadmeFile(model: ClientModel) {
273
209
  const metadata = createMetadata(model) ?? {};
274
- const readmeFileContents = hbs.compile(
275
- model.options ? azureReadmeModularTemplate : nonBrandedReadmeTemplate,
276
- { noEscape: true },
277
- );
210
+ const content = renderTemplate(azureReadmeModularTemplate, metadata as Record<string, unknown>);
278
211
  return {
279
212
  path: "README.md",
280
- content: readmeFileContents(metadata),
213
+ content,
281
214
  };
282
215
  }
283
216
 
@@ -301,7 +234,10 @@ export function updateReadmeFile(
301
234
  try {
302
235
  const metadata = createMetadata(model) ?? {};
303
236
 
304
- const newApiRefLink = hbs.compile(apiReferenceTemplate, { noEscape: true })(metadata).trim();
237
+ const newApiRefLink = renderTemplate(
238
+ apiReferenceTemplate,
239
+ metadata as Record<string, unknown>,
240
+ ).trim();
305
241
 
306
242
  if (!newApiRefLink) {
307
243
  return { path: "README.md", content: existingReadmeContent };
@@ -329,14 +265,12 @@ function createMetadata(model: ClientModel): Metadata | undefined {
329
265
  return;
330
266
  }
331
267
  // const packageDetails = model.options.packageDetails;
332
- const { packageDetails, azureOutputDirectory, serviceInfo, isTypeSpecTest } = model.options;
268
+ const { packageDetails, azureOutputDirectory, serviceInfo } = model.options;
333
269
 
334
- const azureHuh =
335
- packageDetails?.scopeName === "azure" || packageDetails?.scopeName === "azure-rest";
336
270
  const repoURL = "https://github.com/Azure/azure-sdk-for-js";
337
271
  const relativePackageSourcePath = azureOutputDirectory;
338
272
  const packageSourceURL =
339
- relativePackageSourcePath && repoURL && `${repoURL}/tree/main/${relativePackageSourcePath}`;
273
+ relativePackageSourcePath && `${repoURL}/tree/main/${relativePackageSourcePath}`;
340
274
 
341
275
  const clientPackageName = packageDetails?.name;
342
276
  const clientClassName = getClientName(model);
@@ -363,17 +297,9 @@ function createMetadata(model: ClientModel): Metadata | undefined {
363
297
  packageNPMURL: `https://www.npmjs.com/package/${clientPackageName}`,
364
298
  samplesURL:
365
299
  model.options.generateSample && packageSourceURL ? `${packageSourceURL}/samples` : undefined,
366
- apiRefURL: azureHuh
367
- ? `https://learn.microsoft.com/javascript/api/${clientPackageName}${apiRefUrlQueryParameter}`
368
- : undefined,
300
+ apiRefURL: `https://learn.microsoft.com/javascript/api/${clientPackageName}${apiRefUrlQueryParameter}`,
369
301
  azureArm: Boolean(model.options.azureArm),
370
- azure: azureHuh,
371
- isReleasablePackage: !isTypeSpecTest,
372
- repoURL: repoURL,
373
- projectName: azureHuh ? "Microsoft Azure SDK for JavaScript" : undefined,
374
- identityPackageURL: repoURL && `${repoURL}/tree/main/sdk/identity/identity`,
375
302
  addCredentials: model.options.addCredentials,
376
- contributingGuideURL: repoURL && `${repoURL}/blob/main/CONTRIBUTING.md`,
377
303
  generateTest: model.options.generateTest,
378
304
  hasSubscriptionId: model.options.hasSubscriptionId,
379
305
  };
@@ -0,0 +1,202 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+
4
+ /**
5
+ * A tiny, dependency-free renderer for the small subset of Handlebars/Mustache
6
+ * syntax used by the emitter's metadata templates. It intentionally supports only
7
+ * what those templates need:
8
+ *
9
+ * - `{{ name }}` interpolation (always "unescaped", matching the previous
10
+ * `{ noEscape: true }` Handlebars option).
11
+ * - `{{#if name}} ... {{else}} ... {{/if}}` conditional blocks (nesting allowed).
12
+ *
13
+ * It reproduces Handlebars' "standalone" whitespace handling so that a block tag
14
+ * occupying its own line does not leave a blank line behind, keeping the rendered
15
+ * output byte-for-byte identical to the previous Handlebars-based implementation.
16
+ */
17
+
18
+ type TemplateData = Record<string, unknown>;
19
+
20
+ interface TextToken {
21
+ kind: "text";
22
+ value: string;
23
+ }
24
+
25
+ interface TagToken {
26
+ kind: "open" | "else" | "close" | "var";
27
+ /** Condition (for `open`) or variable name (for `var`). */
28
+ name: string;
29
+ /** Whether this block tag stands alone on its line (Handlebars semantics). */
30
+ standalone: boolean;
31
+ }
32
+
33
+ type Token = TextToken | TagToken;
34
+
35
+ const MUSTACHE = /\{\{([^}]*)\}\}/g;
36
+
37
+ function tokenize(template: string): Token[] {
38
+ const tokens: Token[] = [];
39
+ let lastIndex = 0;
40
+ let match: RegExpExecArray | null;
41
+ MUSTACHE.lastIndex = 0;
42
+ while ((match = MUSTACHE.exec(template)) !== null) {
43
+ tokens.push({ kind: "text", value: template.slice(lastIndex, match.index) });
44
+ const inner = match[1]!.trim();
45
+ if (inner.startsWith("#if ")) {
46
+ tokens.push({ kind: "open", name: inner.slice(4).trim(), standalone: false });
47
+ } else if (inner === "else") {
48
+ tokens.push({ kind: "else", name: "", standalone: false });
49
+ } else if (inner === "/if") {
50
+ tokens.push({ kind: "close", name: "", standalone: false });
51
+ } else {
52
+ tokens.push({ kind: "var", name: inner, standalone: false });
53
+ }
54
+ lastIndex = match.index + match[0].length;
55
+ }
56
+ tokens.push({ kind: "text", value: template.slice(lastIndex) });
57
+ return tokens;
58
+ }
59
+
60
+ /**
61
+ * Applies Handlebars "standalone" whitespace stripping. A block tag (`#if`,
62
+ * `else`, `/if`) that is the only non-whitespace content on its line has the
63
+ * surrounding indentation and trailing newline removed.
64
+ */
65
+ function stripStandaloneWhitespace(tokens: Token[]): void {
66
+ const isBlock = (t: Token): t is TagToken =>
67
+ t.kind === "open" || t.kind === "else" || t.kind === "close";
68
+
69
+ // First pass: detect standalone tags from the original text tokens.
70
+ for (let i = 0; i < tokens.length; i++) {
71
+ const token = tokens[i]!;
72
+ if (!isBlock(token)) {
73
+ continue;
74
+ }
75
+ const prev = tokens[i - 1] as TextToken | undefined;
76
+ const next = tokens[i + 1] as TextToken | undefined;
77
+ const prevIsFirst = i - 1 === 0;
78
+ const nextIsLast = i + 1 === tokens.length - 1;
79
+
80
+ const prevValue = prev?.value ?? "";
81
+ const nextValue = next?.value ?? "";
82
+
83
+ const prevStandalone =
84
+ /\n[ \t]*$/.test(prevValue) || (prevIsFirst && /^[ \t]*$/.test(prevValue));
85
+ const nextStandalone =
86
+ /^[ \t]*\n/.test(nextValue) || (nextIsLast && /^[ \t]*$/.test(nextValue));
87
+
88
+ token.standalone = prevStandalone && nextStandalone;
89
+ }
90
+
91
+ // Second pass: strip whitespace around standalone tags.
92
+ for (let i = 0; i < tokens.length; i++) {
93
+ const token = tokens[i]!;
94
+ if (!isBlock(token) || !token.standalone) {
95
+ continue;
96
+ }
97
+ const prev = tokens[i - 1] as TextToken | undefined;
98
+ const next = tokens[i + 1] as TextToken | undefined;
99
+ if (prev) {
100
+ // Drop the indentation preceding the tag, keeping the previous newline.
101
+ prev.value = prev.value.replace(/[ \t]*$/, "");
102
+ }
103
+ if (next) {
104
+ // Drop the rest of the tag's line, including its trailing newline.
105
+ next.value = /^[ \t]*\n/.test(next.value)
106
+ ? next.value.replace(/^[ \t]*\n/, "")
107
+ : next.value.replace(/^[ \t]*$/, "");
108
+ }
109
+ }
110
+ }
111
+
112
+ type Node = TextNode | VarNode | IfNode;
113
+ interface TextNode {
114
+ kind: "text";
115
+ value: string;
116
+ }
117
+ interface VarNode {
118
+ kind: "var";
119
+ name: string;
120
+ }
121
+ interface IfNode {
122
+ kind: "if";
123
+ condition: string;
124
+ whenTrue: Node[];
125
+ whenFalse: Node[];
126
+ }
127
+
128
+ function parse(tokens: Token[], start: number, stopKinds: Token["kind"][]): [Node[], number] {
129
+ const nodes: Node[] = [];
130
+ let i = start;
131
+ while (i < tokens.length) {
132
+ const token = tokens[i]!;
133
+ if (token.kind === "text") {
134
+ if (token.value) {
135
+ nodes.push({ kind: "text", value: token.value });
136
+ }
137
+ i++;
138
+ } else if (token.kind === "var") {
139
+ nodes.push({ kind: "var", name: token.name });
140
+ i++;
141
+ } else if (token.kind === "open") {
142
+ const [whenTrue, afterTrue] = parse(tokens, i + 1, ["else", "close"]);
143
+ let whenFalse: Node[] = [];
144
+ let next = afterTrue;
145
+ if (tokens[next]?.kind === "else") {
146
+ const [elseNodes, afterElse] = parse(tokens, next + 1, ["close"]);
147
+ whenFalse = elseNodes;
148
+ next = afterElse;
149
+ }
150
+ // `next` now points at the matching `close` token.
151
+ nodes.push({ kind: "if", condition: token.name, whenTrue, whenFalse });
152
+ i = next + 1;
153
+ } else if (stopKinds.includes(token.kind)) {
154
+ return [nodes, i];
155
+ } else {
156
+ // Unmatched `else`/`close`; skip defensively.
157
+ i++;
158
+ }
159
+ }
160
+ return [nodes, i];
161
+ }
162
+
163
+ function isTruthy(value: unknown): boolean {
164
+ if (Array.isArray(value)) {
165
+ return value.length > 0;
166
+ }
167
+ return Boolean(value);
168
+ }
169
+
170
+ function renderNodes(nodes: Node[], data: TemplateData): string {
171
+ let out = "";
172
+ for (const node of nodes) {
173
+ switch (node.kind) {
174
+ case "text":
175
+ out += node.value;
176
+ break;
177
+ case "var": {
178
+ const value = data[node.name];
179
+ out += value === undefined || value === null ? "" : String(value);
180
+ break;
181
+ }
182
+ case "if":
183
+ out += renderNodes(isTruthy(data[node.condition]) ? node.whenTrue : node.whenFalse, data);
184
+ break;
185
+ }
186
+ }
187
+ return out;
188
+ }
189
+
190
+ /**
191
+ * Renders a metadata template against the provided data.
192
+ *
193
+ * @param template - Template using the supported `{{var}}` / `{{#if}}` subset.
194
+ * @param data - Values referenced by the template.
195
+ * @returns The rendered string.
196
+ */
197
+ export function renderTemplate(template: string, data: TemplateData): string {
198
+ const tokens = tokenize(template);
199
+ stripStandaloneWhitespace(tokens);
200
+ const [nodes] = parse(tokens, 0, []);
201
+ return renderNodes(nodes, data);
202
+ }