diff --git a/www/utils/packages/docs-generator/src/classes/examples/oas.ts b/www/utils/packages/docs-generator/src/classes/examples/oas.ts index 0a7464e0f1ba6..008f883a8677f 100644 --- a/www/utils/packages/docs-generator/src/classes/examples/oas.ts +++ b/www/utils/packages/docs-generator/src/classes/examples/oas.ts @@ -216,11 +216,15 @@ class OasExamplesGenerator { ] if (isAdminAuthenticated) { - exampleArr.push(`-H 'x-medusa-access-token: {api_token}'`) + exampleArr.push(`-H 'Authorization: Bearer {access_token}'`) } else if (isStoreAuthenticated) { exampleArr.push(`-H 'Authorization: Bearer {access_token}'`) } + if (path.startsWith("/store")) { + exampleArr.push(`-H 'x-publishable-api-key: {your_publishable_api_key}'`) + } + if (requestSchema) { const requestData = this.getSchemaRequiredData(requestSchema) diff --git a/www/utils/packages/docs-generator/src/classes/helpers/oas-schema.ts b/www/utils/packages/docs-generator/src/classes/helpers/oas-schema.ts index b79ef75eb3da0..a026018e37ec0 100644 --- a/www/utils/packages/docs-generator/src/classes/helpers/oas-schema.ts +++ b/www/utils/packages/docs-generator/src/classes/helpers/oas-schema.ts @@ -113,7 +113,9 @@ class OasSchemaHelper { }) } - this.schemas.set(schema["x-schemaName"], schema) + if (this.canAddSchema(schema)) { + this.schemas.set(schema["x-schemaName"], schema) + } return { $ref: this.constructSchemaReference(schema["x-schemaName"]), @@ -181,6 +183,36 @@ class OasSchemaHelper { return clonedSchema } + isSchemaEmpty(schema: OpenApiSchema): boolean { + switch (schema.type) { + case "object": + return ( + schema.properties === undefined || + Object.keys(schema.properties).length === 0 + ) + case "array": + return ( + !this.isRefObject(schema.items) && this.isSchemaEmpty(schema.items) + ) + default: + return false + } + } + + canAddSchema(schema: OpenApiSchema): boolean { + if (!schema["x-schemaName"]) { + return false + } + + const existingSchema = this.schemas.get(schema["x-schemaName"]) + + if (!existingSchema) { + return true + } + + return this.isSchemaEmpty(existingSchema) && !this.isSchemaEmpty(schema) + } + /** * Retrieve the expected file name of the schema. * @@ -212,7 +244,7 @@ class OasSchemaHelper { // check if it already exists in the schemas map if (this.schemas.has(schemaName)) { return { - schema: this.schemas.get(schemaName)!, + schema: JSON.parse(JSON.stringify(this.schemas.get(schemaName)!)), schemaPrefix: `@schema ${schemaName}`, } } @@ -272,7 +304,7 @@ class OasSchemaHelper { return name .replace("DTO", "") .replace(this.schemaRefPrefix, "") - .replace(/(? this.MAX_LEVEL) { return {} @@ -1354,6 +1374,7 @@ class OasKindGenerator extends FunctionKindGenerator { parentName: title || descriptionOptions?.parentName, } : undefined, + saveSchema, ...rest, }), } @@ -1384,6 +1405,7 @@ class OasKindGenerator extends FunctionKindGenerator { level, title, descriptionOptions, + saveSchema, ...rest, }) ) @@ -1407,6 +1429,7 @@ class OasKindGenerator extends FunctionKindGenerator { level, title, descriptionOptions, + saveSchema, ...rest, }) }) @@ -1437,6 +1460,7 @@ class OasKindGenerator extends FunctionKindGenerator { level, descriptionOptions, allowedChildren: pickedProperties, + saveSchema, ...rest, }) case typeAsString.startsWith("Omit"): @@ -1458,6 +1482,7 @@ class OasKindGenerator extends FunctionKindGenerator { level, descriptionOptions, disallowedChildren: omitProperties, + saveSchema, ...rest, }) case typeAsString.startsWith("Partial"): @@ -1475,6 +1500,7 @@ class OasKindGenerator extends FunctionKindGenerator { descriptionOptions, disallowedChildren, allowedChildren, + saveSchema, ...rest, }) @@ -1485,13 +1511,29 @@ class OasKindGenerator extends FunctionKindGenerator { case itemType.isClassOrInterface() || itemType.isTypeParameter() || (itemType as ts.Type).flags === ts.TypeFlags.Object: - const properties: Record = {} + const properties: Record< + string, + OpenApiSchema | OpenAPIV3.ReferenceObject + > = {} const requiredProperties: string[] = [] const baseType = itemType.getBaseTypes()?.[0] const isDeleteResponse = baseType?.aliasSymbol?.getEscapedName() === "DeleteResponse" + const objSchema: OpenApiSchema = { + type: "object", + description, + "x-schemaName": + itemType.isClassOrInterface() || + itemType.isTypeParameter() || + (isZodObject(itemType) && zodObjectTypeName) + ? this.oasSchemaHelper.normalizeSchemaName(typeAsString) + : undefined, + // this is changed later + required: undefined, + } + if (level + 1 <= this.MAX_LEVEL) { itemType.getProperties().forEach((property) => { if ( @@ -1504,6 +1546,41 @@ class OasKindGenerator extends FunctionKindGenerator { requiredProperties.push(property.name) } const propertyType = this.checker.getTypeOfSymbol(property) + + // if property's type is same as parent's property, + // create a reference to the parent + const arrHasParentType = + this.checker.isArrayType(propertyType) && + this.areTypesEqual( + itemType, + this.checker.getTypeArguments( + propertyType as ts.TypeReference + )[0] + ) + const isParentType = this.areTypesEqual(itemType, propertyType) + + if (isParentType && objSchema["x-schemaName"]) { + properties[property.name] = { + $ref: this.oasSchemaHelper.constructSchemaReference( + objSchema["x-schemaName"] + ), + } + + return + } else if (arrHasParentType && objSchema["x-schemaName"]) { + properties[property.name] = { + type: "array", + description, + items: { + $ref: this.oasSchemaHelper.constructSchemaReference( + objSchema["x-schemaName"] + ), + }, + } as OpenAPIV3.ArraySchemaObject + + return + } + properties[property.name] = this.typeToSchema({ itemType: propertyType, level: level + 1, @@ -1513,38 +1590,34 @@ class OasKindGenerator extends FunctionKindGenerator { typeStr: property.name, parentName: title || descriptionOptions?.parentName, }, + saveSchema, ...rest, }) - if (isDeleteResponse && property.name === "object") { + if ( + isDeleteResponse && + property.name === "object" && + !this.oasSchemaHelper.isRefObject(properties[property.name]) + ) { + const schemaProperty = properties[property.name] as OpenApiSchema // try to retrieve default from `DeleteResponse`'s type argument const deleteTypeArg = baseType.aliasTypeArguments?.[0] - properties[property.name].default = + schemaProperty.default = deleteTypeArg && "value" in deleteTypeArg ? (deleteTypeArg.value as string) - : properties[property.name].default + : schemaProperty.default } }) } - const objSchema: OpenApiSchema = { - type: "object", - description, - "x-schemaName": - itemType.isClassOrInterface() || - itemType.isTypeParameter() || - (isZodObject(itemType) && zodObjectTypeName) - ? this.oasSchemaHelper.normalizeSchemaName(typeAsString) - : undefined, - required: - requiredProperties.length > 0 ? requiredProperties : undefined, - } - if (Object.values(properties).length) { objSchema.properties = properties } - if (objSchema["x-schemaName"]) { + objSchema.required = + requiredProperties.length > 0 ? requiredProperties : undefined + + if (saveSchema && objSchema["x-schemaName"]) { // add object to schemas to be created // if necessary this.oasSchemaHelper.namedSchemaToReference(objSchema) @@ -1947,8 +2020,6 @@ class OasKindGenerator extends FunctionKindGenerator { }) || oldSchemaObj.items } - // update schema - if ( oldSchemaObj!.description !== newSchemaObj?.description && oldSchemaObj!.description === SUMMARY_PLACEHOLDER @@ -2184,6 +2255,10 @@ class OasKindGenerator extends FunctionKindGenerator { return true }) } + + private areTypesEqual(type1: ts.Type, type2: ts.Type): boolean { + return "id" in type1 && "id" in type2 && type1.id === type2.id + } } export default OasKindGenerator diff --git a/www/utils/packages/typedoc-generate-references/src/constants/custom-options.ts b/www/utils/packages/typedoc-generate-references/src/constants/custom-options.ts index 53841d352e62e..9118163cb0645 100644 --- a/www/utils/packages/typedoc-generate-references/src/constants/custom-options.ts +++ b/www/utils/packages/typedoc-generate-references/src/constants/custom-options.ts @@ -17,9 +17,9 @@ const customOptions: Record> = { name: "core-flows", plugin: ["typedoc-plugin-workflows"], enableWorkflowsPlugins: true, - enableNamespaceGenerator: true, + enablePathNamespaceGenerator: true, // @ts-expect-error there's a typing issue in typedoc - generateNamespaces: getCoreFlowNamespaces(), + generatePathNamespaces: getCoreFlowNamespaces(), }), "auth-provider": getOptions({ entryPointPath: "packages/core/utils/src/auth/abstract-auth-provider.ts", @@ -35,6 +35,7 @@ const customOptions: Record> = { ], tsConfigName: "utils.json", name: "dml", + generateCustomNamespaces: true, }), file: getOptions({ entryPointPath: "packages/core/utils/src/file/abstract-file-provider.ts", diff --git a/www/utils/packages/typedoc-plugin-custom/README.md b/www/utils/packages/typedoc-plugin-custom/README.md index 371e46c6b36f8..dc29b8b01ef0f 100644 --- a/www/utils/packages/typedoc-plugin-custom/README.md +++ b/www/utils/packages/typedoc-plugin-custom/README.md @@ -37,7 +37,7 @@ The following options are useful for linting: ### Generate Namespace Plugin -If the `generateNamespaces` option is enabled, Namespaces are created from reflections having the `@customNamespace` tag. It also attaches categories (using the `@category` tag) of the same reflection to its generated parent namespace. +If the `generatePathNamespaces` option is enabled, Namespaces are created from reflections having the `@customNamespace` tag. It also attaches categories (using the `@category` tag) of the same reflection to its generated parent namespace. It also accepts the following options: diff --git a/www/utils/packages/typedoc-plugin-custom/src/generate-custom-namespaces.ts b/www/utils/packages/typedoc-plugin-custom/src/generate-custom-namespaces.ts new file mode 100644 index 0000000000000..6582f155496e2 --- /dev/null +++ b/www/utils/packages/typedoc-plugin-custom/src/generate-custom-namespaces.ts @@ -0,0 +1,279 @@ +import { + Application, + Comment, + CommentDisplayPart, + CommentTag, + Context, + Converter, + DeclarationReflection, + ParameterType, + Reflection, + ReflectionCategory, + ReflectionKind, +} from "typedoc" + +type PluginOptions = { + generatePathNamespaces: boolean + parentNamespace: string + namePrefix: string +} + +export class GenerateCustomNamespacePlugin { + private options?: PluginOptions + private app: Application + private parentNamespace?: DeclarationReflection + private currentNamespaceHeirarchy: DeclarationReflection[] + private currentContext?: Context + private scannedComments = false + + constructor(app: Application) { + this.app = app + this.currentNamespaceHeirarchy = [] + this.declareOptions() + + this.app.converter.on( + Converter.EVENT_RESOLVE, + this.handleCreateDeclarationEvent.bind(this) + ) + this.app.converter.on( + Converter.EVENT_CREATE_DECLARATION, + this.scanComments.bind(this) + ) + } + + declareOptions() { + this.app.options.addDeclaration({ + name: "generateCustomNamespaces", + type: ParameterType.Boolean, + defaultValue: false, + help: "Whether to enable conversion of categories to namespaces.", + }) + this.app.options.addDeclaration({ + name: "customParentNamespace", + type: ParameterType.String, + defaultValue: "", + help: "Optionally specify a parent namespace to place all generated namespaces in.", + }) + this.app.options.addDeclaration({ + name: "customNamespaceNamePrefix", + type: ParameterType.String, + defaultValue: "", + help: "Optionally specify a name prefix for all namespaces.", + }) + } + + readOptions() { + if (this.options) { + return + } + + this.options = { + generatePathNamespaces: this.app.options.getValue( + "generateCustomNamespaces" + ), + parentNamespace: this.app.options.getValue("customParentNamespace"), + namePrefix: this.app.options.getValue("customNamespaceNamePrefix"), + } + } + + loadNamespace(namespaceName: string): DeclarationReflection { + const formattedName = this.formatName(namespaceName) + return this.currentContext?.project + .getReflectionsByKind(ReflectionKind.Namespace) + .find( + (m) => + m.name === formattedName && + (!this.currentNamespaceHeirarchy.length || + m.parent?.id === + this.currentNamespaceHeirarchy[ + this.currentNamespaceHeirarchy.length - 1 + ].id) + ) as DeclarationReflection + } + + createNamespace(namespaceName: string): DeclarationReflection | undefined { + if (!this.currentContext) { + return + } + const formattedName = this.formatName(namespaceName) + const namespace = this.currentContext?.createDeclarationReflection( + ReflectionKind.Namespace, + void 0, + void 0, + formattedName + ) + + namespace.children = [] + + return namespace + } + + formatName(namespaceName: string): string { + return `${this.options?.namePrefix}${namespaceName}` + } + + generateNamespaceFromTag({ + tag, + summary, + }: { + tag: CommentTag + reflection?: DeclarationReflection + summary?: CommentDisplayPart[] + }) { + const categoryHeirarchy = tag.content[0].text.split(".") + categoryHeirarchy.forEach((cat, index) => { + // check whether a namespace exists with the category name. + let namespace = this.loadNamespace(cat) + + if (!namespace) { + // add a namespace for this category + namespace = this.createNamespace(cat) || namespace + + namespace.comment = new Comment() + if (this.currentNamespaceHeirarchy.length) { + namespace.comment.modifierTags.add("@namespaceMember") + } + if (summary && index === categoryHeirarchy.length - 1) { + namespace.comment.summary = summary + } + } + this.currentContext = + this.currentContext?.withScope(namespace) || this.currentContext + + this.currentNamespaceHeirarchy.push(namespace) + }) + } + + /** + * create categories in the last namespace if the + * reflection has a category + */ + attachCategories( + reflection: DeclarationReflection, + comments: Comment | undefined + ) { + if (!this.currentNamespaceHeirarchy.length) { + return + } + + const parentNamespace = + this.currentNamespaceHeirarchy[this.currentNamespaceHeirarchy.length - 1] + comments?.blockTags + .filter((tag) => tag.tag === "@category") + .forEach((tag) => { + const categoryName = tag.content[0].text + if (!parentNamespace.categories) { + parentNamespace.categories = [] + } + let category = parentNamespace.categories.find( + (category) => category.title === categoryName + ) + if (!category) { + category = new ReflectionCategory(categoryName) + parentNamespace.categories.push(category) + } + category.children.push(reflection) + }) + } + + handleCreateDeclarationEvent(context: Context, reflection: Reflection) { + if (!(reflection instanceof DeclarationReflection)) { + return + } + this.readOptions() + if (this.options?.parentNamespace && !this.parentNamespace) { + this.parentNamespace = + this.loadNamespace(this.options.parentNamespace) || + this.createNamespace(this.options.parentNamespace) + } + this.currentNamespaceHeirarchy = [] + if (this.parentNamespace) { + this.currentNamespaceHeirarchy.push(this.parentNamespace) + } + this.currentContext = context + const comments = this.getReflectionComments(reflection) + comments?.blockTags + .filter((tag) => tag.tag === "@customNamespace") + .forEach((tag) => { + this.generateNamespaceFromTag({ + tag, + }) + if ( + reflection.parent instanceof DeclarationReflection || + reflection.parent?.isProject() + ) { + reflection.parent.children = reflection.parent.children?.filter( + (child) => child.id !== reflection.id + ) + } + this.currentContext?.addChild(reflection) + }) + + comments?.removeTags("@customNamespace") + this.attachCategories(reflection, comments) + this.currentContext = undefined + this.currentNamespaceHeirarchy = [] + } + + /** + * Scan all source files for `@customNamespace` tag to generate namespaces + * This is mainly helpful to pull summaries of the namespaces. + */ + scanComments(context: Context) { + if (this.scannedComments) { + return + } + this.currentContext = context + const fileNames = context.program.getRootFileNames() + + fileNames.forEach((fileName) => { + const sourceFile = context.program.getSourceFile(fileName) + if (!sourceFile) { + return + } + + const comments = context.getFileComment(sourceFile) + comments?.blockTags + .filter((tag) => tag.tag === "@customNamespace") + .forEach((tag) => { + this.generateNamespaceFromTag({ tag, summary: comments.summary }) + if (this.currentNamespaceHeirarchy.length) { + // add comments of the file to the last created namespace + this.currentNamespaceHeirarchy[ + this.currentNamespaceHeirarchy.length - 1 + ].comment = comments + + this.currentNamespaceHeirarchy[ + this.currentNamespaceHeirarchy.length - 1 + ].comment!.blockTags = this.currentNamespaceHeirarchy[ + this.currentNamespaceHeirarchy.length - 1 + ].comment!.blockTags.filter((tag) => tag.tag !== "@customNamespace") + } + // reset values + this.currentNamespaceHeirarchy = [] + this.currentContext = context + }) + }) + + this.scannedComments = true + } + + getReflectionComments( + reflection: DeclarationReflection + ): Comment | undefined { + if (reflection.comment) { + return reflection.comment + } + + // try to retrieve comment from signature + if (!reflection.signatures?.length) { + return + } + return reflection.signatures.find((signature) => signature.comment)?.comment + } + + // for debugging + printCurrentHeirarchy() { + return this.currentNamespaceHeirarchy.map((heirarchy) => heirarchy.name) + } +} diff --git a/www/utils/packages/typedoc-plugin-custom/src/generate-namespace.ts b/www/utils/packages/typedoc-plugin-custom/src/generate-path-namespaces.ts similarity index 86% rename from www/utils/packages/typedoc-plugin-custom/src/generate-namespace.ts rename to www/utils/packages/typedoc-plugin-custom/src/generate-path-namespaces.ts index ccfbe11e0554a..e8e3dbe79db5d 100644 --- a/www/utils/packages/typedoc-plugin-custom/src/generate-namespace.ts +++ b/www/utils/packages/typedoc-plugin-custom/src/generate-path-namespaces.ts @@ -12,13 +12,13 @@ import { NamespaceGenerateDetails } from "types" export function load(app: Application) { app.options.addDeclaration({ - name: "enableNamespaceGenerator", + name: "enablePathNamespaceGenerator", type: ParameterType.Boolean, defaultValue: false, help: "Whether to enable the namespace generator plugin.", }) app.options.addDeclaration({ - name: "generateNamespaces", + name: "generatePathNamespaces", type: ParameterType.Mixed, defaultValue: [], help: "The namespaces to generate.", @@ -27,15 +27,15 @@ export function load(app: Application) { const generatedNamespaces: Map = new Map() app.converter.on(Converter.EVENT_BEGIN, (context) => { - if (!app.options.getValue("enableNamespaceGenerator")) { + if (!app.options.getValue("enablePathNamespaceGenerator")) { return } const namespaces = app.options.getValue( - "generateNamespaces" + "generatePathNamespaces" ) as unknown as NamespaceGenerateDetails[] - const generateNamespaces = (ns: NamespaceGenerateDetails[]) => { + const generatePathNamespaces = (ns: NamespaceGenerateDetails[]) => { const createdNamespaces: DeclarationReflection[] = [] ns.forEach((namespace) => { const genNamespace = createNamespace(context, namespace) @@ -43,7 +43,7 @@ export function load(app: Application) { generatedNamespaces.set(namespace.pathPattern, genNamespace) if (namespace.children) { - generateNamespaces(namespace.children).forEach((child) => + generatePathNamespaces(namespace.children).forEach((child) => genNamespace.addChild(child) ) } @@ -54,13 +54,13 @@ export function load(app: Application) { return createdNamespaces } - generateNamespaces(namespaces) + generatePathNamespaces(namespaces) }) app.converter.on( Converter.EVENT_CREATE_DECLARATION, (context, reflection) => { - if (!app.options.getValue("enableNamespaceGenerator")) { + if (!app.options.getValue("enablePathNamespaceGenerator")) { return } @@ -72,7 +72,7 @@ export function load(app: Application) { } const namespaces = app.options.getValue( - "generateNamespaces" + "generatePathNamespaces" ) as unknown as NamespaceGenerateDetails[] const findNamespace = ( diff --git a/www/utils/packages/typedoc-plugin-custom/src/index.ts b/www/utils/packages/typedoc-plugin-custom/src/index.ts index 54720d5aa4da8..dd6009744a511 100644 --- a/www/utils/packages/typedoc-plugin-custom/src/index.ts +++ b/www/utils/packages/typedoc-plugin-custom/src/index.ts @@ -7,11 +7,12 @@ import { load as eslintExamplePlugin } from "./eslint-example" import { load as signatureModifierPlugin } from "./signature-modifier" import { MermaidDiagramGenerator } from "./mermaid-diagram-generator" import { load as parentIgnorePlugin } from "./parent-ignore" -import { load as generateNamespacePlugin } from "./generate-namespace" +import { load as generateNamespacePlugin } from "./generate-path-namespaces" import { DmlRelationsResolver } from "./dml-relations-resolver" import { load as dmlTypesNormalizer } from "./dml-types-normalizer" import { MermaidDiagramDMLGenerator } from "./mermaid-diagram-dml-generator" import { load as dmlJsonParser } from "./dml-json-parser" +import { GenerateCustomNamespacePlugin } from "./generate-custom-namespaces" export function load(app: Application) { resolveReferencesPluginLoad(app) @@ -28,4 +29,5 @@ export function load(app: Application) { new MermaidDiagramGenerator(app) new DmlRelationsResolver(app) new MermaidDiagramDMLGenerator(app) + new GenerateCustomNamespacePlugin(app) } diff --git a/www/utils/packages/types/lib/index.d.ts b/www/utils/packages/types/lib/index.d.ts index f243f69cb6c0d..920eb18762310 100644 --- a/www/utils/packages/types/lib/index.d.ts +++ b/www/utils/packages/types/lib/index.d.ts @@ -251,14 +251,27 @@ export declare module "typedoc" { */ enableWorkflowsPlugins: boolean /** - * Whether to enable the namespace generator plugin. + * Whether to enable the namespace generator plugin for paths. * @defaultValue false */ - enableNamespaceGenerator: boolean + enablePathNamespaceGenerator: boolean /** - * The namespaces to generate. + * The namespaces to generate for paths. */ - generateNamespaces: NamespaceGenerateDetails[] + generatePathNamespaces: NamespaceGenerateDetails[] + /** + * Whether to enable the namespace generator plugin for `@customNamespaces` usage. + * @defaultValue false + */ + generateCustomNamespaces: boolean + /** + * Optionally specify a parent namespace to place all generated custom namespaces in. + */ + customParentNamespace: string + /** + * Optionally specify a name prefix for all custom namespaces. + */ + customNamespaceNamePrefix: string } }