From 61d3bdde4abd31bd2f606f5b70a2e88b2f17a442 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Wed, 21 May 2025 14:25:40 +0300 Subject: [PATCH] docs-util: extract feature flags in generated OAS (#12554) --- .../src/classes/kinds/default.ts | 7 ++ .../docs-generator/src/classes/kinds/oas.ts | 75 ++++++++++++++++++- .../docs-generator/src/types/index.d.ts | 2 + 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/www/utils/packages/docs-generator/src/classes/kinds/default.ts b/www/utils/packages/docs-generator/src/classes/kinds/default.ts index 300baea8c3..c25c443b97 100644 --- a/www/utils/packages/docs-generator/src/classes/kinds/default.ts +++ b/www/utils/packages/docs-generator/src/classes/kinds/default.ts @@ -605,10 +605,12 @@ class DefaultKindGenerator { getInformationFromTags(node: ts.Node): { deprecatedTag: ts.JSDocTag | undefined versionTag: ts.JSDocTag | undefined + featureFlagTag: ts.JSDocTag | undefined } { const nodeComments = ts.getJSDocCommentsAndTags(node) let deprecatedTag: ts.JSDocTag | undefined let versionTag: ts.JSDocTag | undefined + let featureFlagTag: ts.JSDocTag | undefined nodeComments.forEach((comment) => { if (!("tags" in comment)) { @@ -623,12 +625,17 @@ class DefaultKindGenerator { if (tag.tagName.getText() === "version") { versionTag = tag } + + if (tag.tagName.getText() === "featureFlag") { + featureFlagTag = tag + } }) }) return { deprecatedTag, versionTag, + featureFlagTag, } } } diff --git a/www/utils/packages/docs-generator/src/classes/kinds/oas.ts b/www/utils/packages/docs-generator/src/classes/kinds/oas.ts index bf12c8b5f7..0919097520 100644 --- a/www/utils/packages/docs-generator/src/classes/kinds/oas.ts +++ b/www/utils/packages/docs-generator/src/classes/kinds/oas.ts @@ -435,7 +435,8 @@ class OasKindGenerator extends FunctionKindGenerator { } // check deprecation and version in tags - const { deprecatedTag, versionTag } = this.getInformationFromTags(node) + const { deprecatedTag, versionTag, featureFlagTag } = + this.getInformationFromTags(node) if (deprecatedTag) { oas.deprecated = true @@ -450,6 +451,12 @@ class OasKindGenerator extends FunctionKindGenerator { : undefined } + if (featureFlagTag) { + oas["x-featureFlag"] = featureFlagTag.comment + ? (featureFlagTag.comment as string) + : undefined + } + return formatOas(oas, oasPrefix) } @@ -784,7 +791,8 @@ class OasKindGenerator extends FunctionKindGenerator { } // check deprecation and version in tags - const { deprecatedTag, versionTag } = this.getInformationFromTags(node) + const { deprecatedTag, versionTag, featureFlagTag } = + this.getInformationFromTags(node) if (deprecatedTag) { oas.deprecated = true @@ -804,6 +812,14 @@ class OasKindGenerator extends FunctionKindGenerator { delete oas["x-version"] } + if (featureFlagTag) { + oas["x-featureFlag"] = featureFlagTag.comment + ? (featureFlagTag.comment as string) + : undefined + } else { + delete oas["x-featureFlag"] + } + return formatOas(oas, oasPrefix) } @@ -1562,6 +1578,27 @@ class OasKindGenerator extends FunctionKindGenerator { }) }) || undefined // avoid showing it as false in the generated OAS + let featureFlag: string | undefined + + commentsAndTags.some((comment) => { + if (!("tags" in comment)) { + return false + } + + comment.tags?.some((tag) => { + if (tag.tagName.getText() !== "featureFlag" || !tag.comment) { + return false + } + + featureFlag = + typeof tag.comment === "string" ? tag.comment : tag.comment.join(" ") + + return true + }) + + return featureFlag !== undefined + }) + switch (true) { case isEnum || isEnumParent: const enumMembers: string[] = [] @@ -1584,6 +1621,7 @@ class OasKindGenerator extends FunctionKindGenerator { description, enum: enumMembers, deprecated: isDeprecated, + "x-featureFlag": featureFlag, } case itemType.isLiteral() || typeAsString === "RegExp": const isString = @@ -1602,6 +1640,7 @@ class OasKindGenerator extends FunctionKindGenerator { name: title, }), deprecated: isDeprecated, + "x-featureFlag": featureFlag, } case itemType.flags === ts.TypeFlags.String || itemType.flags === ts.TypeFlags.Number || @@ -1622,6 +1661,7 @@ class OasKindGenerator extends FunctionKindGenerator { name: title, }), deprecated: isDeprecated, + "x-featureFlag": featureFlag, } case ("intrinsicName" in itemType && itemType.intrinsicName === "boolean") || @@ -1634,6 +1674,7 @@ class OasKindGenerator extends FunctionKindGenerator { ? this.getDefaultValue(symbol?.valueDeclaration) : undefined, deprecated: isDeprecated, + "x-featureFlag": featureFlag, } case this.checker.isArrayType(itemType): return { @@ -1656,6 +1697,7 @@ class OasKindGenerator extends FunctionKindGenerator { saveSchema, ...rest, }), + "x-featureFlag": featureFlag, } case itemType.isUnion(): // if it's a union of literal types, @@ -1675,6 +1717,7 @@ class OasKindGenerator extends FunctionKindGenerator { enum: cleanedUpTypes.map( (unionType) => (unionType as ts.LiteralType).value ), + "x-featureFlag": featureFlag, } } @@ -1690,12 +1733,17 @@ class OasKindGenerator extends FunctionKindGenerator { ) if (oneOfItems.length === 1) { - return oneOfItems[0] + return { + ...oneOfItems[0], + "x-featureFlag": oneOfItems[0]["x-featureFlag"] || featureFlag, + deprecated: oneOfItems[0].deprecated || isDeprecated, + } } return { oneOf: oneOfItems, deprecated: isDeprecated, + "x-featureFlag": featureFlag, } case itemType.isIntersection(): const allOfItems = this.typesHelper @@ -1712,12 +1760,17 @@ class OasKindGenerator extends FunctionKindGenerator { }) if (allOfItems.length === 1) { - return allOfItems[0] + return { + ...allOfItems[0], + "x-featureFlag": allOfItems[0]["x-featureFlag"] || featureFlag, + deprecated: allOfItems[0].deprecated || isDeprecated, + } } return { allOf: allOfItems, deprecated: isDeprecated, + "x-featureFlag": featureFlag, } case typeAsString.startsWith("Pick"): const pickTypeArgs = @@ -1807,6 +1860,7 @@ class OasKindGenerator extends FunctionKindGenerator { : undefined, // this is changed later required: undefined, + "x-featureFlag": featureFlag, } const properties: Record< @@ -2472,6 +2526,19 @@ class OasKindGenerator extends FunctionKindGenerator { } } + if (oldSchemaObj?.["x-featureFlag"] !== newSchemaObj?.["x-featureFlag"]) { + // avoid many changes to exising OAS + if (!newSchemaObj?.["x-featureFlag"]) { + if (oldSchemaObj!["x-featureFlag"]) { + wasUpdated = true + } + delete oldSchemaObj!["x-featureFlag"] + } else { + oldSchemaObj!["x-featureFlag"] = newSchemaObj["x-featureFlag"] + wasUpdated = true + } + } + if (!wasUpdated) { const requiredChanged = oldSchemaObj!.required?.length !== newSchemaObj?.required?.length || diff --git a/www/utils/packages/docs-generator/src/types/index.d.ts b/www/utils/packages/docs-generator/src/types/index.d.ts index 968f4693d1..902c0d157e 100644 --- a/www/utils/packages/docs-generator/src/types/index.d.ts +++ b/www/utils/packages/docs-generator/src/types/index.d.ts @@ -13,6 +13,7 @@ export declare type OpenApiOperation = Partial & { "x-events"?: OasEvent[] "x-deprecated_message"?: string "x-version"?: string + "x-featureFlag"?: string } export declare type CommonCliOptions = { @@ -23,6 +24,7 @@ export declare type CommonCliOptions = { export declare type OpenApiSchema = OpenAPIV3.SchemaObject & { "x-schemaName"?: string + "x-featureFlag"?: string } export declare interface OpenApiTagObject extends OpenAPIV3.TagObject {