@zimic/http 0.0.1-canary.2

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 (61) hide show
  1. package/LICENSE.md +16 -0
  2. package/README.md +230 -0
  3. package/dist/chunk-VHQRAQPQ.mjs +1371 -0
  4. package/dist/chunk-VHQRAQPQ.mjs.map +1 -0
  5. package/dist/chunk-VUDGONB5.js +1382 -0
  6. package/dist/chunk-VUDGONB5.js.map +1 -0
  7. package/dist/cli.js +116 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/cli.mjs +109 -0
  10. package/dist/cli.mjs.map +1 -0
  11. package/dist/index.d.ts +1306 -0
  12. package/dist/index.js +544 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/index.mjs +537 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/dist/typegen.d.ts +86 -0
  17. package/dist/typegen.js +12 -0
  18. package/dist/typegen.js.map +1 -0
  19. package/dist/typegen.mjs +3 -0
  20. package/dist/typegen.mjs.map +1 -0
  21. package/index.d.ts +1 -0
  22. package/package.json +110 -0
  23. package/src/cli/cli.ts +92 -0
  24. package/src/cli/index.ts +4 -0
  25. package/src/cli/typegen/openapi.ts +24 -0
  26. package/src/formData/HttpFormData.ts +300 -0
  27. package/src/formData/types.ts +110 -0
  28. package/src/headers/HttpHeaders.ts +217 -0
  29. package/src/headers/types.ts +65 -0
  30. package/src/index.ts +55 -0
  31. package/src/pathParams/types.ts +67 -0
  32. package/src/searchParams/HttpSearchParams.ts +258 -0
  33. package/src/searchParams/types.ts +133 -0
  34. package/src/typegen/index.ts +12 -0
  35. package/src/typegen/namespace/TypegenNamespace.ts +18 -0
  36. package/src/typegen/openapi/generate.ts +168 -0
  37. package/src/typegen/openapi/transform/components.ts +481 -0
  38. package/src/typegen/openapi/transform/context.ts +67 -0
  39. package/src/typegen/openapi/transform/filters.ts +71 -0
  40. package/src/typegen/openapi/transform/imports.ts +15 -0
  41. package/src/typegen/openapi/transform/io.ts +86 -0
  42. package/src/typegen/openapi/transform/methods.ts +803 -0
  43. package/src/typegen/openapi/transform/operations.ts +120 -0
  44. package/src/typegen/openapi/transform/paths.ts +119 -0
  45. package/src/typegen/openapi/utils/types.ts +45 -0
  46. package/src/types/arrays.d.ts +4 -0
  47. package/src/types/json.ts +89 -0
  48. package/src/types/objects.d.ts +14 -0
  49. package/src/types/requests.ts +96 -0
  50. package/src/types/schema.ts +834 -0
  51. package/src/types/strings.d.ts +9 -0
  52. package/src/types/utils.ts +64 -0
  53. package/src/utils/console.ts +7 -0
  54. package/src/utils/data.ts +13 -0
  55. package/src/utils/files.ts +28 -0
  56. package/src/utils/imports.ts +12 -0
  57. package/src/utils/prettier.ts +13 -0
  58. package/src/utils/strings.ts +3 -0
  59. package/src/utils/time.ts +25 -0
  60. package/src/utils/urls.ts +52 -0
  61. package/typegen.d.ts +1 -0
@@ -0,0 +1,168 @@
1
+ import filesystem from 'fs/promises';
2
+ import path from 'path';
3
+ import ts from 'typescript';
4
+
5
+ import { isDefined } from '@/utils/data';
6
+
7
+ import {
8
+ isComponentsDeclaration,
9
+ normalizeComponents,
10
+ populateReferencedComponents,
11
+ removeUnreferencedComponents,
12
+ } from './transform/components';
13
+ import { createTypeTransformationContext, TypeTransformContext } from './transform/context';
14
+ import { readPathFiltersFromFile, ignoreEmptyFilters } from './transform/filters';
15
+ import { createImportDeclarations } from './transform/imports';
16
+ import {
17
+ convertTypesToString,
18
+ importTypesFromOpenAPI,
19
+ prepareTypeOutputToSave,
20
+ writeTypeOutputToStandardOutput,
21
+ } from './transform/io';
22
+ import { isOperationsDeclaration, normalizeOperations, removeUnreferencedOperations } from './transform/operations';
23
+ import { isPathsDeclaration, normalizePaths } from './transform/paths';
24
+
25
+ const RESOURCES_TO_REMOVE_IF_NOT_NORMALIZED = ['paths', 'webhooks', 'operations', 'components', '$defs'];
26
+
27
+ function removeUnknownResources(node: ts.Node | undefined) {
28
+ const isUnknownResource =
29
+ !node ||
30
+ ((ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) &&
31
+ RESOURCES_TO_REMOVE_IF_NOT_NORMALIZED.includes(node.name.text));
32
+
33
+ if (isUnknownResource) {
34
+ return undefined;
35
+ }
36
+
37
+ return node;
38
+ }
39
+
40
+ function normalizeRawNodes(rawNodes: ts.Node[], context: TypeTransformContext, options: { prune: boolean }) {
41
+ let normalizedNodes = rawNodes.map((node) => (isPathsDeclaration(node) ? normalizePaths(node, context) : node));
42
+
43
+ if (options.prune) {
44
+ normalizedNodes = normalizedNodes
45
+ .map((node) => (isOperationsDeclaration(node) ? removeUnreferencedOperations(node, context) : node))
46
+ .filter(isDefined);
47
+ }
48
+
49
+ normalizedNodes = normalizedNodes
50
+ .map((node) => (isOperationsDeclaration(node) ? normalizeOperations(node, context) : node))
51
+ .filter(isDefined);
52
+
53
+ if (options.prune) {
54
+ for (const node of normalizedNodes) {
55
+ if (isComponentsDeclaration(node, context)) {
56
+ populateReferencedComponents(node, context);
57
+ }
58
+ }
59
+
60
+ normalizedNodes = normalizedNodes
61
+ .map((node) => (isComponentsDeclaration(node, context) ? removeUnreferencedComponents(node, context) : node))
62
+ .filter(isDefined);
63
+ }
64
+
65
+ normalizedNodes = normalizedNodes
66
+ .map((node) => (isComponentsDeclaration(node, context) ? normalizeComponents(node, context) : node))
67
+ .map(removeUnknownResources)
68
+ .filter(isDefined);
69
+
70
+ return normalizedNodes;
71
+ }
72
+
73
+ /**
74
+ * The options to use when generating types from an OpenAPI schema.
75
+ *
76
+ * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐typegen#typegengeneratefromopenapioptions `typegen.generateFromOpenAPI(options)` API reference}
77
+ */
78
+ export interface OpenAPITypegenOptions {
79
+ /**
80
+ * The path to a local OpenAPI schema file or an URL to fetch it. Version 3 is supported as YAML or JSON.
81
+ *
82
+ * @example
83
+ * './schema.yaml';
84
+ * 'https://example.com/openapi/schema.yaml';
85
+ */
86
+ input: string;
87
+ /**
88
+ * The path to write the generated types to. If not provided, the types will be written to stdout.
89
+ *
90
+ * @example
91
+ * './schema.ts';
92
+ */
93
+ output?: string;
94
+ /**
95
+ * The name of the service to use in the generated types.
96
+ *
97
+ * @example
98
+ * 'MyService';
99
+ */
100
+ serviceName: string;
101
+ /** Whether to include comments in the generated types. */
102
+ includeComments: boolean;
103
+ /**
104
+ * Whether to remove unused operations and components from the generated types. This is useful for reducing the size
105
+ * of the output file.
106
+ */
107
+ prune: boolean;
108
+ /**
109
+ * One or more expressions to filter the types to generate. Filters must follow the format `<method> <path>`, where
110
+ * `<method>` is an HTTP method or `*`, and `<path>` is a literal path or a glob. Filters are case-sensitive regarding
111
+ * paths. Negative filters can be created by prefixing the expression with `!`. If more than one positive filter is
112
+ * provided, they will be combined with OR, while negative filters will be combined with AND.
113
+ *
114
+ * @example
115
+ * ['GET /users', '* /users', 'GET,POST /users/*', 'DELETE /users/**\\/*', '!GET /notifications'];
116
+ */
117
+ filters?: string[];
118
+ /**
119
+ * A path to a file containing filter expressions. One expression is expected per line and the format is the same as
120
+ * used in a `--filter` option. Comments are prefixed with `#`. A filter file can be used alongside additional
121
+ * `--filter` expressions.
122
+ *
123
+ * @example
124
+ * './filters.txt';
125
+ */
126
+ filterFile?: string;
127
+ }
128
+
129
+ /**
130
+ * Generates TypeScript types from an OpenAPI schema.
131
+ *
132
+ * @param options The options to use when generating the types.
133
+ * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐typegen#typegengeneratefromopenapioptions `typegen.generateFromOpenAPI(options)` API reference}
134
+ */
135
+ async function generateTypesFromOpenAPI({
136
+ input: inputFilePathOrURL,
137
+ output: outputFilePath,
138
+ serviceName,
139
+ includeComments,
140
+ prune,
141
+ filters: filtersFromArguments = [],
142
+ filterFile,
143
+ }: OpenAPITypegenOptions) {
144
+ const filtersFromFile = filterFile ? await readPathFiltersFromFile(filterFile) : [];
145
+ const filters = ignoreEmptyFilters([...filtersFromFile, ...filtersFromArguments]);
146
+
147
+ const rawNodes = await importTypesFromOpenAPI(inputFilePathOrURL);
148
+ const context = createTypeTransformationContext(serviceName, filters);
149
+ const nodes = normalizeRawNodes(rawNodes, context, { prune });
150
+
151
+ const importDeclarations = createImportDeclarations(context);
152
+
153
+ for (const declaration of importDeclarations) {
154
+ nodes.unshift(declaration);
155
+ }
156
+
157
+ const typeOutput = await convertTypesToString(nodes, { includeComments });
158
+ const formattedOutput = prepareTypeOutputToSave(typeOutput);
159
+
160
+ const shouldWriteToStdout = outputFilePath === undefined;
161
+ if (shouldWriteToStdout) {
162
+ await writeTypeOutputToStandardOutput(formattedOutput);
163
+ } else {
164
+ await filesystem.writeFile(path.resolve(outputFilePath), formattedOutput);
165
+ }
166
+ }
167
+
168
+ export default generateTypesFromOpenAPI;
@@ -0,0 +1,481 @@
1
+ import ts from 'typescript';
2
+
3
+ import { Override } from '@/types/utils';
4
+ import { isDefined } from '@/utils/data';
5
+
6
+ import { isNeverType, isUnknownType } from '../utils/types';
7
+ import { ComponentPath, TypeTransformContext } from './context';
8
+ import { normalizeContentType, normalizeResponse } from './methods';
9
+ import { normalizePath } from './paths';
10
+
11
+ export function createComponentsIdentifierText(serviceName: string) {
12
+ return `${serviceName}Components`;
13
+ }
14
+
15
+ function createComponentsIdentifier(serviceName: string) {
16
+ return ts.factory.createIdentifier(createComponentsIdentifierText(serviceName));
17
+ }
18
+
19
+ type ComponentsDeclaration = ts.InterfaceDeclaration;
20
+
21
+ export function isComponentsDeclaration(
22
+ node: ts.Node | undefined,
23
+ context: TypeTransformContext,
24
+ ): node is ComponentsDeclaration {
25
+ const componentIdentifiers = ['components', createComponentsIdentifierText(context.serviceName)];
26
+ return node !== undefined && ts.isInterfaceDeclaration(node) && componentIdentifiers.includes(node.name.text);
27
+ }
28
+
29
+ type ComponentGroup = Override<
30
+ ts.PropertySignature,
31
+ {
32
+ type: ts.TypeLiteralNode;
33
+ name: ts.Identifier | ts.StringLiteral;
34
+ }
35
+ >;
36
+
37
+ function isComponentGroup(node: ts.TypeElement): node is ComponentGroup {
38
+ return (
39
+ ts.isPropertySignature(node) &&
40
+ node.type !== undefined &&
41
+ ts.isTypeLiteralNode(node.type) &&
42
+ ts.isIdentifier(node.name)
43
+ );
44
+ }
45
+
46
+ type Component = Override<
47
+ ts.PropertySignature,
48
+ {
49
+ type: ts.TypeNode;
50
+ name: ts.Identifier | ts.StringLiteral;
51
+ }
52
+ >;
53
+
54
+ function isComponent(node: ts.TypeElement): node is Component {
55
+ return (
56
+ ts.isPropertySignature(node) &&
57
+ node.type !== undefined &&
58
+ !isNeverType(node.type) &&
59
+ (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name))
60
+ );
61
+ }
62
+
63
+ type RequestComponent = Override<Component, { type: ts.TypeLiteralNode }>;
64
+
65
+ function isRequestComponent(node: Component): node is RequestComponent {
66
+ return ts.isTypeLiteralNode(node.type);
67
+ }
68
+
69
+ function unchangedIndexedAccessTypeNode(node: ts.IndexedAccessTypeNode) {
70
+ return node;
71
+ }
72
+
73
+ function visitComponentReferences(
74
+ node: ts.TypeNode,
75
+ context: TypeTransformContext & {
76
+ isComponentIndexedAccess?: boolean;
77
+ partialComponentPath?: string[];
78
+ },
79
+ options: {
80
+ onComponentReference: (node: ts.IndexedAccessTypeNode, componentPath: ComponentPath) => void;
81
+ renameComponentReference?: (
82
+ node: ts.IndexedAccessTypeNode,
83
+ resources: {
84
+ objectType: ts.TypeReferenceNode;
85
+ indexType: ts.LiteralTypeNode;
86
+ componentGroupName: string;
87
+ },
88
+ ) => ts.IndexedAccessTypeNode;
89
+ },
90
+ ): ts.TypeNode {
91
+ const { onComponentReference, renameComponentReference = unchangedIndexedAccessTypeNode } = options;
92
+
93
+ if (isUnknownType(node)) {
94
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
95
+ }
96
+
97
+ if (ts.isTypeReferenceNode(node)) {
98
+ const newTypeArguments = node.typeArguments?.map((type) => visitComponentReferences(type, context, options));
99
+ return ts.factory.updateTypeReferenceNode(node, node.typeName, ts.factory.createNodeArray(newTypeArguments));
100
+ }
101
+
102
+ if (ts.isArrayTypeNode(node)) {
103
+ const newElementType = visitComponentReferences(node.elementType, context, options);
104
+ return ts.factory.updateArrayTypeNode(node, newElementType);
105
+ }
106
+
107
+ if (ts.isTupleTypeNode(node)) {
108
+ const newElements = node.elements.map((element) => visitComponentReferences(element, context, options));
109
+ return ts.factory.updateTupleTypeNode(node, ts.factory.createNodeArray(newElements));
110
+ }
111
+
112
+ if (ts.isUnionTypeNode(node)) {
113
+ const newTypes = node.types.map((type) => visitComponentReferences(type, context, options));
114
+ return ts.factory.updateUnionTypeNode(node, ts.factory.createNodeArray(newTypes));
115
+ }
116
+
117
+ if (ts.isIntersectionTypeNode(node)) {
118
+ const newTypes = node.types.map((type) => visitComponentReferences(type, context, options));
119
+ return ts.factory.updateIntersectionTypeNode(node, ts.factory.createNodeArray(newTypes));
120
+ }
121
+
122
+ if (ts.isParenthesizedTypeNode(node)) {
123
+ const newType = visitComponentReferences(node.type, context, options);
124
+ return ts.factory.updateParenthesizedType(node, newType);
125
+ }
126
+
127
+ if (ts.isTypeLiteralNode(node)) {
128
+ const newMembers = node.members.map((member) => {
129
+ if (ts.isPropertySignature(member) && member.type) {
130
+ const newType = visitComponentReferences(member.type, context, options);
131
+ return ts.factory.updatePropertySignature(member, member.modifiers, member.name, member.questionToken, newType);
132
+ }
133
+
134
+ /* istanbul ignore else -- @preserve */
135
+ if (ts.isIndexSignatureDeclaration(member)) {
136
+ const newType = visitComponentReferences(member.type, context, options);
137
+ return ts.factory.updateIndexSignature(member, member.modifiers, member.parameters, newType);
138
+ }
139
+
140
+ /* istanbul ignore next -- @preserve
141
+ * All members are expected to be either a property signature or an index signature. */
142
+ return member;
143
+ });
144
+
145
+ return ts.factory.updateTypeLiteralNode(node, ts.factory.createNodeArray(newMembers));
146
+ }
147
+
148
+ if (ts.isIndexedAccessTypeNode(node)) {
149
+ const isRootIndexedAccess = context.isComponentIndexedAccess ?? true;
150
+
151
+ if (ts.isIndexedAccessTypeNode(node.objectType)) {
152
+ const childContext: typeof context = { ...context, isComponentIndexedAccess: false };
153
+ const newObjectType = visitComponentReferences(node.objectType, childContext, options);
154
+
155
+ const newNode = ts.factory.updateIndexedAccessTypeNode(node, newObjectType, node.indexType);
156
+
157
+ /* istanbul ignore else -- @preserve
158
+ * Component indexed accesses are always expected to have child indexed accesses. */
159
+ if (childContext.partialComponentPath && childContext.partialComponentPath.length > 0) {
160
+ const hasIndexTypeName =
161
+ ts.isLiteralTypeNode(node.indexType) &&
162
+ (ts.isIdentifier(node.indexType.literal) || ts.isStringLiteral(node.indexType.literal));
163
+
164
+ /* istanbul ignore else -- @preserve
165
+ * Component indexed accesses are always expected to have child indexed accesses. */
166
+ if (hasIndexTypeName) {
167
+ const componentName = node.indexType.literal.text;
168
+ childContext.partialComponentPath.push(componentName);
169
+ }
170
+
171
+ /* istanbul ignore else -- @preserve
172
+ * Component indexed accesses are always expected to have child indexed accesses. */
173
+ if (isRootIndexedAccess) {
174
+ const componentGroupName = childContext.partialComponentPath[0];
175
+ const componentName = childContext.partialComponentPath.slice(1).join('.');
176
+ const componentPath = `${componentGroupName}.${componentName}` as const;
177
+ onComponentReference(newNode, componentPath);
178
+ }
179
+ }
180
+
181
+ return newNode;
182
+ }
183
+
184
+ const componentIdentifiers = ['components', createComponentsIdentifierText(context.serviceName)];
185
+
186
+ const isComponentIndexedAccess =
187
+ ts.isTypeReferenceNode(node.objectType) &&
188
+ ts.isIdentifier(node.objectType.typeName) &&
189
+ componentIdentifiers.includes(node.objectType.typeName.text) &&
190
+ ts.isLiteralTypeNode(node.indexType) &&
191
+ (ts.isIdentifier(node.indexType.literal) || ts.isStringLiteral(node.indexType.literal));
192
+
193
+ /* istanbul ignore else -- @preserve
194
+ * All indexed accesses are expected to point to components. */
195
+ if (isComponentIndexedAccess) {
196
+ const isRawComponent = node.objectType.typeName.text === 'components';
197
+ const componentGroupName = node.indexType.literal.text;
198
+
199
+ const newNode = isRawComponent
200
+ ? renameComponentReference(node, {
201
+ objectType: node.objectType,
202
+ indexType: node.indexType,
203
+ componentGroupName,
204
+ })
205
+ : node;
206
+
207
+ const newNodeHasComponentGroupName =
208
+ ts.isLiteralTypeNode(newNode.indexType) &&
209
+ (ts.isIdentifier(newNode.indexType.literal) || ts.isStringLiteral(newNode.indexType.literal));
210
+
211
+ /* istanbul ignore else -- @preserve
212
+ * All component indexed accesses are expected to have an index type name. */
213
+ if (newNodeHasComponentGroupName) {
214
+ const newComponentGroupName = newNode.indexType.literal.text;
215
+ context.partialComponentPath = [newComponentGroupName];
216
+ }
217
+
218
+ return newNode;
219
+ }
220
+ }
221
+
222
+ return node;
223
+ }
224
+
225
+ export function normalizeComponentGroupName(rawComponentGroupName: string) {
226
+ if (rawComponentGroupName === 'requestBodies') {
227
+ return 'requests';
228
+ }
229
+ return rawComponentGroupName;
230
+ }
231
+
232
+ export function renameComponentReferences(node: ts.TypeNode, context: TypeTransformContext): ts.TypeNode {
233
+ return visitComponentReferences(node, context, {
234
+ onComponentReference(_node, componentPath) {
235
+ context.referencedTypes.components.add(componentPath);
236
+ },
237
+
238
+ renameComponentReference(node, { indexType, objectType, componentGroupName }) {
239
+ const newIdentifier = createComponentsIdentifier(context.serviceName);
240
+ const newObjectType = ts.factory.updateTypeReferenceNode(objectType, newIdentifier, objectType.typeArguments);
241
+
242
+ const newComponentGroupName = normalizeComponentGroupName(componentGroupName);
243
+ const newIndexType = ts.factory.updateLiteralTypeNode(
244
+ indexType,
245
+ ts.factory.createStringLiteral(newComponentGroupName),
246
+ );
247
+
248
+ return ts.factory.updateIndexedAccessTypeNode(node, newObjectType, newIndexType);
249
+ },
250
+ });
251
+ }
252
+
253
+ function processPendingRequestComponentActions(component: RequestComponent, context: TypeTransformContext) {
254
+ const pendingRequestActions = context.pendingActions.components.requests;
255
+
256
+ const componentName = component.name.text;
257
+ const shouldBeMarkedAsOptional = pendingRequestActions.toMarkBodyAsOptional.has(componentName);
258
+
259
+ const bodyQuestionToken = shouldBeMarkedAsOptional
260
+ ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
261
+ : component.questionToken;
262
+
263
+ pendingRequestActions.toMarkBodyAsOptional.delete(componentName);
264
+
265
+ return { bodyQuestionToken };
266
+ }
267
+
268
+ function wrapRequestComponentType(type: ts.TypeNode, context: TypeTransformContext) {
269
+ context.typeImports.http.add('HttpSchema');
270
+
271
+ const httpSchemaRequestWrapper = ts.factory.createQualifiedName(
272
+ ts.factory.createIdentifier('HttpSchema'),
273
+ ts.factory.createIdentifier('Request'),
274
+ );
275
+ return ts.factory.createTypeReferenceNode(httpSchemaRequestWrapper, [type]);
276
+ }
277
+
278
+ function normalizeRequestComponent(component: Component, context: TypeTransformContext) {
279
+ /* istanbul ignore if -- @preserve
280
+ * Component group members in `requests` are always expected the be request components. */
281
+ if (!isRequestComponent(component)) {
282
+ return undefined;
283
+ }
284
+
285
+ const { bodyQuestionToken } = processPendingRequestComponentActions(component, context);
286
+ const newType = normalizeContentType(component.type, context, { bodyQuestionToken });
287
+
288
+ return ts.factory.updatePropertySignature(
289
+ component,
290
+ component.modifiers,
291
+ component.name,
292
+ component.questionToken,
293
+ wrapRequestComponentType(newType, context),
294
+ );
295
+ }
296
+
297
+ function normalizeComponent(
298
+ component: ts.TypeElement,
299
+ componentGroupName: string,
300
+ context: TypeTransformContext,
301
+ ): ts.TypeElement | undefined {
302
+ /* istanbul ignore if -- @preserve
303
+ * Component group members are always expected the be components. */
304
+ if (!isComponent(component)) {
305
+ return undefined;
306
+ }
307
+
308
+ if (componentGroupName === 'requests') {
309
+ return normalizeRequestComponent(component, context);
310
+ }
311
+
312
+ if (componentGroupName === 'responses') {
313
+ const responseComponent = normalizeResponse(component, context, { isComponent: true });
314
+ return responseComponent?.newSignature;
315
+ }
316
+
317
+ if (componentGroupName === 'pathItems') {
318
+ return normalizePath(component, context, { isComponent: true });
319
+ }
320
+
321
+ return ts.factory.updatePropertySignature(
322
+ component,
323
+ component.modifiers,
324
+ component.name,
325
+ component.questionToken,
326
+ renameComponentReferences(component.type, context),
327
+ );
328
+ }
329
+
330
+ function normalizeComponentGroup(componentGroup: ts.TypeElement, context: TypeTransformContext) {
331
+ if (!isComponentGroup(componentGroup)) {
332
+ return undefined;
333
+ }
334
+
335
+ const componentGroupName = normalizeComponentGroupName(componentGroup.name.text);
336
+ const newIdentifier = ts.factory.createIdentifier(componentGroupName);
337
+
338
+ const newComponents = componentGroup.type.members
339
+ .map((component) => normalizeComponent(component, componentGroupName, context))
340
+ .filter(isDefined);
341
+
342
+ const newType = ts.factory.updateTypeLiteralNode(componentGroup.type, ts.factory.createNodeArray(newComponents));
343
+
344
+ return ts.factory.updatePropertySignature(
345
+ componentGroup,
346
+ componentGroup.modifiers,
347
+ newIdentifier,
348
+ componentGroup.questionToken,
349
+ newType,
350
+ );
351
+ }
352
+
353
+ export function normalizeComponents(components: ts.InterfaceDeclaration, context: TypeTransformContext) {
354
+ const newIdentifier = createComponentsIdentifier(context.serviceName);
355
+
356
+ const newMembers = components.members
357
+ .map((componentGroup) => normalizeComponentGroup(componentGroup, context))
358
+ .filter(isDefined);
359
+
360
+ return ts.factory.updateInterfaceDeclaration(
361
+ components,
362
+ components.modifiers,
363
+ newIdentifier,
364
+ components.typeParameters,
365
+ components.heritageClauses,
366
+ newMembers,
367
+ );
368
+ }
369
+
370
+ export function populateReferencedComponents(components: ts.InterfaceDeclaration, context: TypeTransformContext) {
371
+ const pathsToVisit = new Set(context.referencedTypes.components);
372
+
373
+ while (pathsToVisit.size > 0) {
374
+ const previousPathsToVisit = new Set(pathsToVisit);
375
+ pathsToVisit.clear();
376
+
377
+ for (const componentGroup of components.members) {
378
+ if (!isComponentGroup(componentGroup)) {
379
+ continue;
380
+ }
381
+
382
+ const componentGroupName = normalizeComponentGroupName(componentGroup.name.text);
383
+
384
+ for (const component of componentGroup.type.members) {
385
+ /* istanbul ignore if -- @preserve
386
+ * Component group members are always expected the be components. */
387
+ if (!isComponent(component)) {
388
+ continue;
389
+ }
390
+
391
+ const componentName = component.name.text;
392
+ const componentPath = `${componentGroupName}.${componentName}` as const;
393
+ const isComponentToVisit = previousPathsToVisit.has(componentPath);
394
+
395
+ if (!isComponentToVisit) {
396
+ continue;
397
+ }
398
+
399
+ context.referencedTypes.components.add(componentPath);
400
+
401
+ visitComponentReferences(component.type, context, {
402
+ onComponentReference(_node, componentPath) {
403
+ const isKnownReferencedComponent = context.referencedTypes.components.has(componentPath);
404
+ if (!isKnownReferencedComponent) {
405
+ pathsToVisit.add(componentPath);
406
+ }
407
+ },
408
+ });
409
+ }
410
+ }
411
+ }
412
+ }
413
+
414
+ export function removeComponentIfUnreferenced(
415
+ component: ts.TypeElement,
416
+ componentGroupName: string,
417
+ context: TypeTransformContext,
418
+ ) {
419
+ /* istanbul ignore if -- @preserve
420
+ * Component group members are always expected the be components. */
421
+ if (!isComponent(component)) {
422
+ return undefined;
423
+ }
424
+
425
+ const componentName = component.name.text;
426
+ const componentPath = `${componentGroupName}.${componentName}` as const;
427
+
428
+ if (context.referencedTypes.components.has(componentPath)) {
429
+ context.referencedTypes.components.delete(componentPath);
430
+ return component;
431
+ }
432
+
433
+ return undefined;
434
+ }
435
+
436
+ function removeUnreferencedComponentsInGroup(componentGroup: ts.TypeElement, context: TypeTransformContext) {
437
+ /* istanbul ignore if -- @preserve
438
+ * Component members are always expected the be component groups. */
439
+ if (!isComponentGroup(componentGroup)) {
440
+ return undefined;
441
+ }
442
+
443
+ const componentGroupName = normalizeComponentGroupName(componentGroup.name.text);
444
+
445
+ const newComponents = componentGroup.type.members
446
+ .map((component) => removeComponentIfUnreferenced(component, componentGroupName, context))
447
+ .filter(isDefined);
448
+
449
+ if (newComponents.length === 0) {
450
+ return undefined;
451
+ }
452
+
453
+ return ts.factory.updatePropertySignature(
454
+ componentGroup,
455
+ componentGroup.modifiers,
456
+ componentGroup.name,
457
+ componentGroup.questionToken,
458
+ ts.factory.updateTypeLiteralNode(componentGroup.type, ts.factory.createNodeArray(newComponents)),
459
+ );
460
+ }
461
+
462
+ export function removeUnreferencedComponents(components: ts.InterfaceDeclaration, context: TypeTransformContext) {
463
+ const newComponentGroups = components.members
464
+ .map((componentGroup) => removeUnreferencedComponentsInGroup(componentGroup, context))
465
+ .filter(isDefined);
466
+
467
+ context.referencedTypes.components.clear();
468
+
469
+ if (newComponentGroups.length === 0) {
470
+ return undefined;
471
+ }
472
+
473
+ return ts.factory.updateInterfaceDeclaration(
474
+ components,
475
+ components.modifiers,
476
+ components.name,
477
+ components.typeParameters,
478
+ components.heritageClauses,
479
+ newComponentGroups,
480
+ );
481
+ }