docs-util: added a namespaces plugin (#8500)

Added a namespaces plugin that allows generating namespaces and automatically adds reflections in a path pattern to the namespace.

This is particularly useful for the workflows reference, as workflows under the `**/packages/core/core-flows/**/workflows/**` path pattern are shown under a Workflows title, and steps under the path pattern `**/packages/core/core-flows/**/steps/**` are shown under a Steps title.
This commit is contained in:
Shahed Nasser
2024-08-08 16:03:42 +03:00
committed by GitHub
parent c21407afbe
commit fe200e0bb3
7 changed files with 110 additions and 266 deletions

View File

@@ -16,6 +16,22 @@ const customOptions: Record<string, Partial<TypeDocOptions>> = {
name: "core-flows",
plugin: ["typedoc-plugin-workflows"],
enableWorkflowsPlugins: true,
enableNamespaceGenerator: true,
// @ts-expect-error there's a typing issue in typedoc
generateNamespaces: [
{
name: "Workflows",
description:
"Workflows listed here are created by Medusa and can be imported from `@medusajs/core-flows`.",
pathPattern: "**/packages/core/core-flows/**/workflows/**",
},
{
name: "Steps",
description:
"Steps listed here are created by Medusa and can be imported from `@medusajs/core-flows`.",
pathPattern: "**/packages/core/core-flows/**/steps/**",
},
],
}),
"auth-provider": getOptions({
entryPointPath: "packages/core/utils/src/auth/abstract-auth-provider.ts",
@@ -31,7 +47,6 @@ const customOptions: Record<string, Partial<TypeDocOptions>> = {
],
tsConfigName: "utils.json",
name: "dml",
generateNamespaces: true,
}),
file: getOptions({
entryPointPath: "packages/core/utils/src/file/abstract-file-provider.ts",

View File

@@ -36,6 +36,7 @@
"dependencies": {
"eslint": "^8.53.0",
"glob": "^10.3.10",
"minimatch": "^10.0.1",
"utils": "*",
"yaml": "^2.3.3"
}

View File

@@ -1,277 +1,81 @@
import { minimatch } from "minimatch"
import {
Application,
Comment,
CommentDisplayPart,
CommentTag,
Context,
Converter,
DeclarationReflection,
ParameterType,
Reflection,
ReflectionCategory,
ReflectionKind,
} from "typedoc"
import { NamespaceGenerateDetails } from "types"
type PluginOptions = {
generateNamespaces: boolean
parentNamespace: string
namePrefix: string
}
export function load(app: Application) {
app.options.addDeclaration({
name: "enableNamespaceGenerator",
type: ParameterType.Boolean,
defaultValue: false,
help: "Whether to enable the namespace generator plugin.",
})
app.options.addDeclaration({
name: "generateNamespaces",
type: ParameterType.Mixed,
defaultValue: [],
help: "The namespaces to generate.",
})
export class GenerateNamespacePlugin {
private options?: PluginOptions
private app: Application
private parentNamespace?: DeclarationReflection
private currentNamespaceHeirarchy: DeclarationReflection[]
private currentContext?: Context
private scannedComments = false
const generatedNamespaces: Map<string, DeclarationReflection> = new Map()
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: "generateNamespaces",
type: ParameterType.Boolean,
defaultValue: false,
help: "Whether to enable conversion of categories to namespaces.",
})
this.app.options.addDeclaration({
name: "parentNamespace",
type: ParameterType.String,
defaultValue: "",
help: "Optionally specify a parent namespace to place all generated namespaces in.",
})
this.app.options.addDeclaration({
name: "namePrefix",
type: ParameterType.String,
defaultValue: "",
help: "Optionally specify a name prefix for all namespaces.",
})
}
readOptions() {
if (this.options) {
app.converter.on(Converter.EVENT_BEGIN, (context) => {
if (!app.options.getValue("enableNamespaceGenerator")) {
return
}
this.options = {
generateNamespaces: this.app.options.getValue("generateNamespaces"),
parentNamespace: this.app.options.getValue("parentNamespace"),
namePrefix: this.app.options.getValue("namePrefix"),
}
}
const namespaces = app.options.getValue(
"generateNamespaces"
) as unknown as NamespaceGenerateDetails[]
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
}
namespaces.forEach((namespace) => {
const genNamespace = context.createDeclarationReflection(
ReflectionKind.Namespace,
void 0,
void 0,
namespace.name
)
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
}
if (namespace.description) {
genNamespace.comment = new Comment([
{
kind: "text",
text: namespace.description,
},
])
}
this.currentContext =
this.currentContext?.withScope(namespace) || this.currentContext
this.currentNamespaceHeirarchy.push(namespace)
generatedNamespaces.set(namespace.pathPattern, genNamespace)
})
}
})
/**
* 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) {
app.converter.on(
Converter.EVENT_CREATE_DECLARATION,
(context, reflection) => {
if (!app.options.getValue("enableNamespaceGenerator")) {
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
const symbol = context.project.getSymbolFromReflection(reflection)
const filePath = symbol?.valueDeclaration?.getSourceFile().fileName
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
})
})
if (!filePath) {
return
}
this.scannedComments = true
}
generatedNamespaces.forEach((namespace, pathPattern) => {
if (!minimatch(filePath, pathPattern)) {
return
}
getReflectionComments(
reflection: DeclarationReflection
): Comment | undefined {
if (reflection.comment) {
return reflection.comment
namespace.addChild(reflection)
})
}
// 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)
}
)
}

View File

@@ -7,7 +7,7 @@ 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 { GenerateNamespacePlugin } from "./generate-namespace"
import { load as generateNamespacePlugin } from "./generate-namespace"
import { DmlRelationsResolver } from "./dml-relations-resolver"
import { load as dmlTypesNormalizer } from "./dml-types-normalizer"
import { MermaidDiagramDMLGenerator } from "./mermaid-diagram-dml-generator"
@@ -23,8 +23,8 @@ export function load(app: Application) {
parentIgnorePlugin(app)
dmlTypesNormalizer(app)
dmlJsonParser(app)
generateNamespacePlugin(app)
new GenerateNamespacePlugin(app)
new MermaidDiagramGenerator(app)
new DmlRelationsResolver(app)
new MermaidDiagramDMLGenerator(app)

View File

@@ -197,19 +197,6 @@ export declare module "typedoc" {
* @defaultValue true
*/
outputModules: boolean
/**
* Whether to enable category to namespace conversion.
* @defaultValue false
*/
generateNamespaces: boolean
/**
* Optionally specify a parent namespace to place all generated namespaces in.
*/
parentNamespace: string
/**
* Optionally specify a name prefix for all generated namespaces.
*/
namePrefix: string
/**
* Whether to enable the React Query manipulator.
* @defaultValue false
@@ -262,6 +249,15 @@ export declare module "typedoc" {
* @defaultValue false
*/
enableWorkflowsPlugins: boolean
/**
* Whether to enable the namespace generator plugin.
* @defaultValue false
*/
enableNamespaceGenerator: boolean
/**
* The namespaces to generate.
*/
generateNamespaces: NamespaceGenerateDetails[]
}
}
@@ -273,3 +269,21 @@ export declare type DmlFile = {
properties: DmlObject
}
}
export declare type NamespaceGenerateDetails = {
/**
* The namespace's names.
*/
name: string
/**
* The namespace's description. Will be attached
* as a summary comment.
*/
description?: string
/**
* A path pattern to pass to minimatch that
* checks if a file / its reflections belong to the
* namespace
*/
pathPattern: string
}

View File

@@ -2,7 +2,7 @@ import { SignatureReflection } from "typedoc"
export function isWorkflow(reflection: SignatureReflection): boolean {
return (
reflection.parent.children?.some((child) => child.name === "runAsStep") ||
reflection.parent?.children?.some((child) => child.name === "runAsStep") ||
false
)
}

View File

@@ -3906,6 +3906,15 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:^10.0.1":
version: 10.0.1
resolution: "minimatch@npm:10.0.1"
dependencies:
brace-expansion: ^2.0.1
checksum: e6c29a81fe83e1877ad51348306be2e8aeca18c88fdee7a99df44322314279e15799e41d7cb274e4e8bb0b451a3bc622d6182e157dfa1717d6cda75e9cd8cd5d
languageName: node
linkType: hard
"minimatch@npm:^3.0.2, minimatch@npm:^3.0.3, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2":
version: 3.1.2
resolution: "minimatch@npm:3.1.2"
@@ -5374,6 +5383,7 @@ __metadata:
"@types/node": ^16.11.10
eslint: ^8.53.0
glob: ^10.3.10
minimatch: ^10.0.1
types: "*"
typescript: 5.5
utils: "*"