diff --git a/packages/client/src/compiler.ts b/packages/client/src/compiler.ts index a2006e8..57a666c 100644 --- a/packages/client/src/compiler.ts +++ b/packages/client/src/compiler.ts @@ -40,12 +40,13 @@ export interface Compiler { * tag words array may be provided, in which case the line will be left as * is. */ - renderBlock(blockContent: string, reservedTagWords?: string[]): Block; + renderBlock(blockContent: string, reservedTagWords?: string[]): Promise; /** * Render a string of markdown to HTML, using the options from `Documentalist`. + * If any `marked` plugin is set to `async: true`, the return value will be a Promise. */ - renderMarkdown(markdown: string): string; + renderMarkdown(markdown: string): string | Promise; } /** diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 21c36ab..46129c7 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -23,7 +23,7 @@ "glob": "^10.3.10", "js-yaml": "^4.1.0", "kss": "^3.0.1", - "marked": "^10.0.0", + "marked": "^11.1.1", "tsconfig-resolver": "^3.0.1", "typedoc": "~0.25.2", "yargs": "^17.7.2" diff --git a/packages/compiler/src/__tests__/__snapshots__/typescript.test.ts.snap b/packages/compiler/src/__tests__/__snapshots__/typescript.test.ts.snap index 9a6be5d..4bb02b4 100644 --- a/packages/compiler/src/__tests__/__snapshots__/typescript.test.ts.snap +++ b/packages/compiler/src/__tests__/__snapshots__/typescript.test.ts.snap @@ -1502,7 +1502,9 @@ is.", } `; -exports[`TypescriptPlugin options excludeNames 1`] = ` +exports[`TypescriptPlugin options includePrivateMembers 1`] = `{}`; + +exports[`TypescriptPlugin options includePrivateMembers 2`] = ` [ "active", "disabled", @@ -1513,13 +1515,3 @@ exports[`TypescriptPlugin options excludeNames 1`] = ` "type", ] `; - -exports[`TypescriptPlugin options excludePaths 1`] = `{}`; - -exports[`TypescriptPlugin options includePrivateMembers 1`] = ` -[ - "bark", - "consumePrivate", - "eat", -] -`; diff --git a/packages/compiler/src/__tests__/compilerImpl.test.ts b/packages/compiler/src/__tests__/compilerImpl.test.ts index 17ceabc..7055453 100644 --- a/packages/compiler/src/__tests__/compilerImpl.test.ts +++ b/packages/compiler/src/__tests__/compilerImpl.test.ts @@ -41,33 +41,33 @@ describe("CompilerImpl", () => { const MARKDOWN = "# Title\nbody body body"; const OBJECT = { hello: "world", size: 1000 }; - it("extracts contentsRaw and parses metadata", () => { - const data = API.renderBlock(METADATA + MARKDOWN); + it("extracts contentsRaw and parses metadata", async () => { + const data = await API.renderBlock(METADATA + MARKDOWN); expect(data.contentsRaw).toBe(MARKDOWN); expect(data.metadata).toEqual(OBJECT); }); - it("supports empty metadata block", () => { - const data = API.renderBlock("---\n---\n" + MARKDOWN); + it("supports empty metadata block", async () => { + const data = await API.renderBlock("---\n---\n" + MARKDOWN); expect(data.contentsRaw).toBe(MARKDOWN); expect(data.metadata).toEqual({}); }); - it("metadata block is optional", () => { - const data = API.renderBlock(MARKDOWN); + it("metadata block is optional", async () => { + const data = await API.renderBlock(MARKDOWN); expect(data.contentsRaw).toBe(MARKDOWN); expect(data.metadata).toEqual({}); }); }); describe("rendered contents", () => { - it("returns a single-element array for string without @tags", () => { - const { contents } = API.renderBlock("simple string"); + it("returns a single-element array for string without @tags", async () => { + const { contents } = await API.renderBlock("simple string"); expect(contents).toEqual(["

simple string

\n"]); }); - it("converts @tag to object in array", () => { - const { contents } = API.renderBlock(FILE); + it("converts @tag to object in array", async () => { + const { contents } = await API.renderBlock(FILE); expect(contents).toHaveLength(3); expect(contents[1]).toEqual({ tag: "interface", @@ -75,8 +75,8 @@ describe("CompilerImpl", () => { }); }); - it("converts @#+ to heading tags in array", () => { - const { contents } = API.renderBlock(HEADING_FILE); + it("converts @#+ to heading tags in array", async () => { + const { contents } = await API.renderBlock(HEADING_FILE); expect(contents).toHaveLength(9); const headings = contents.filter(isHeadingTag); expect(headings).toHaveLength(4); @@ -89,8 +89,8 @@ describe("CompilerImpl", () => { }); }); - it("reservedWords will ignore matching @tag", () => { - const { contents } = API.renderBlock(FILE, ["interface"]); + it("reservedWords will ignore matching @tag", async () => { + const { contents } = await API.renderBlock(FILE, ["interface"]); // only one string (reserved @word does not get split to its own entry) expect(contents).toHaveLength(1); // @tag value comes out exactly as written in source, on its own line diff --git a/packages/compiler/src/compilerImpl.ts b/packages/compiler/src/compilerImpl.ts index ca0a376..88e3f2e 100644 --- a/packages/compiler/src/compilerImpl.ts +++ b/packages/compiler/src/compilerImpl.ts @@ -17,7 +17,7 @@ import { Block, Compiler, HeadingTag, StringOrTag } from "@documentalist/client"; import * as yaml from "js-yaml"; import { marked, MarkedOptions } from "marked"; -import { relative } from "path"; +import { relative } from "node:path"; /** * Matches the triple-dash metadata block on the first line of markdown file. @@ -67,9 +67,9 @@ export class CompilerImpl implements Compiler { return relative(sourceBaseDir, path); }; - public renderBlock = (blockContent: string, reservedTagWords = this.options.reservedTags): Block => { + public renderBlock = async (blockContent: string, reservedTagWords = this.options.reservedTags): Promise => { const { contentsRaw, metadata } = this.extractMetadata(blockContent.trim()); - const contents = this.renderContents(contentsRaw, reservedTagWords); + const contents = await this.renderContents(contentsRaw, reservedTagWords); return { contents, contentsRaw, metadata }; }; @@ -80,11 +80,12 @@ export class CompilerImpl implements Compiler { * `contents` option is `html`, the string nodes will also be rendered with * markdown. */ - private renderContents(content: string, reservedTagWords?: string[]) { + private async renderContents(content: string, reservedTagWords?: string[]) { const splitContents = this.parseTags(content, reservedTagWords); - return splitContents - .map(node => (typeof node === "string" ? this.renderMarkdown(node) : node)) - .filter(node => node !== ""); + const renderedContents = await Promise.all( + splitContents.map(node => Promise.resolve(typeof node === "string" ? this.renderMarkdown(node) : node)), + ); + return renderedContents.filter(node => node !== ""); } /** diff --git a/packages/compiler/src/documentalist.ts b/packages/compiler/src/documentalist.ts index 412bde6..90b0342 100644 --- a/packages/compiler/src/documentalist.ts +++ b/packages/compiler/src/documentalist.ts @@ -15,9 +15,10 @@ */ import { File, Plugin } from "@documentalist/client"; -import * as fs from "fs"; import * as glob from "glob"; -import * as path from "path"; +import { readFileSync } from "node:fs"; +import * as path from "node:path"; + import { CompilerImpl, CompilerOptions } from "./compilerImpl"; /** @@ -112,7 +113,7 @@ export class Documentalist { const absolutePath = path.resolve(fileName); return { path: absolutePath, - read: () => fs.readFileSync(absolutePath, "utf8"), + read: () => readFileSync(absolutePath, "utf8"), }; }); } diff --git a/packages/compiler/src/launch.ts b/packages/compiler/src/launch.ts index 0e540ed..57e6b9a 100644 --- a/packages/compiler/src/launch.ts +++ b/packages/compiler/src/launch.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import * as path from "path"; +import { join } from "node:path"; + import { Documentalist } from "./documentalist"; import { NpmPlugin } from "./plugins/npm"; import { TypescriptPlugin } from "./plugins/typescript"; @@ -28,9 +29,9 @@ Documentalist.create() .use(".ts", new TypescriptPlugin()) .use("package.json", new NpmPlugin()) .documentGlobs( - path.join(__dirname, "..", "package.json"), + join(__dirname, "..", "package.json"), // compile test fixtures: - path.join(__dirname, "__tests__", "__fixtures__", "*.ts"), + join(__dirname, "__tests__", "__fixtures__", "*.ts"), // compile our own source code: // path.join(__dirname, "..", "src", "index.ts"), ); diff --git a/packages/compiler/src/plugins/kss.ts b/packages/compiler/src/plugins/kss.ts index 9da042e..147fd3c 100644 --- a/packages/compiler/src/plugins/kss.ts +++ b/packages/compiler/src/plugins/kss.ts @@ -15,8 +15,8 @@ */ import { Compiler, File, KssExample, KssModifier, KssPluginData, Plugin } from "@documentalist/client"; -import * as kss from "kss"; -import * as path from "path"; +import kss from "kss"; +import { dirname } from "node:path"; /** * The `KssPlugin` extracts [KSS doc comments](http://warpspire.com/kss/syntax/) from CSS code (or similar languages). @@ -28,16 +28,16 @@ import * as path from "path"; export class KssPlugin implements Plugin { public constructor(private options: kss.Options = {}) {} - public compile(cssFiles: File[], dm: Compiler): KssPluginData { + public async compile(cssFiles: File[], dm: Compiler): Promise { const styleguide = this.parseFiles(cssFiles); - const sections = styleguide.sections().map(s => convertSection(s, dm)); + const sections = await Promise.all(styleguide.sections().map(s => convertSection(s, dm))); const css = dm.objectify(sections, s => s.reference); return { css }; } private parseFiles(files: File[]) { const input = files.map(file => ({ - base: path.dirname(file.path), + base: dirname(file.path), contents: file.read(), path: file.path, })); @@ -46,19 +46,19 @@ export class KssPlugin implements Plugin { } } -function convertSection(section: kss.KssSection, dm: Compiler): KssExample { +async function convertSection(section: kss.KssSection, dm: Compiler): Promise { return { - documentation: dm.renderMarkdown(section.description()), + documentation: await dm.renderMarkdown(section.description()), markup: section.markup() || "", - markupHtml: dm.renderMarkdown(`\`\`\`html\n${section.markup() || ""}\n\`\`\``), - modifiers: section.modifiers().map(mod => convertModifier(mod, dm)), + markupHtml: await dm.renderMarkdown(`\`\`\`html\n${section.markup() || ""}\n\`\`\``), + modifiers: await Promise.all(section.modifiers().map(mod => convertModifier(mod, dm))), reference: section.reference(), }; } -function convertModifier(mod: kss.KssModifier, dm: Compiler): KssModifier { +async function convertModifier(mod: kss.KssModifier, dm: Compiler): Promise { return { - documentation: dm.renderMarkdown(mod.description()), + documentation: await dm.renderMarkdown(mod.description()), name: mod.name(), }; } diff --git a/packages/compiler/src/plugins/markdown.ts b/packages/compiler/src/plugins/markdown.ts index 1f41e95..7ca8e95 100644 --- a/packages/compiler/src/plugins/markdown.ts +++ b/packages/compiler/src/plugins/markdown.ts @@ -27,7 +27,7 @@ import { Plugin, slugify, } from "@documentalist/client"; -import * as path from "path"; +import { basename, extname } from "node:path"; import { PageMap } from "../page"; export interface MarkdownPluginOptions { @@ -60,8 +60,8 @@ export class MarkdownPlugin implements Plugin { * Reads the given set of markdown files and adds their data to the internal storage. * Returns a plain object mapping page references to their data. */ - public compile(markdownFiles: File[], compiler: Compiler): MarkdownPluginData { - const pageMap = this.buildPageStore(markdownFiles, compiler); + public async compile(markdownFiles: File[], compiler: Compiler): Promise { + const pageMap = await this.buildPageStore(markdownFiles, compiler); // now that we have all known pages, we can resolve @include tags. this.resolveIncludeTags(pageMap); // generate navigation tree after all pages loaded and processed. @@ -89,10 +89,10 @@ export class MarkdownPlugin implements Plugin { } /** Convert each file to PageData and populate store. */ - private buildPageStore(markdownFiles: File[], { relativePath, renderBlock }: Compiler) { + private async buildPageStore(markdownFiles: File[], { relativePath, renderBlock }: Compiler) { const pageMap = new PageMap(); for (const file of markdownFiles) { - const block = renderBlock(file.read()); + const block = await renderBlock(file.read()); const page = this.blockToPage(relativePath(file.path), block); pageMap.set(page.reference, page); } @@ -158,7 +158,7 @@ function getReference(absolutePath: string, { metadata }: Block) { if (metadata.reference != null) { return metadata.reference; } - return path.basename(absolutePath, path.extname(absolutePath)); + return basename(absolutePath, extname(absolutePath)); } function getTitle(block: Block) { diff --git a/packages/compiler/src/plugins/npm.ts b/packages/compiler/src/plugins/npm.ts index c408ce3..c85e73d 100644 --- a/packages/compiler/src/plugins/npm.ts +++ b/packages/compiler/src/plugins/npm.ts @@ -15,7 +15,7 @@ */ import { Compiler, File, NpmPackageInfo, NpmPluginData, Plugin } from "@documentalist/client"; -import { spawnSync } from "child_process"; +import { spawnSync } from "node:child_process"; export interface NpmPluginOptions { /** Whether to exclude packages marked `private`. */ diff --git a/packages/compiler/src/plugins/typescript/typescriptPlugin.ts b/packages/compiler/src/plugins/typescript/typescriptPlugin.ts index a30c011..995ba5a 100644 --- a/packages/compiler/src/plugins/typescript/typescriptPlugin.ts +++ b/packages/compiler/src/plugins/typescript/typescriptPlugin.ts @@ -15,11 +15,12 @@ */ import type { Compiler, File, Plugin, TsDocEntry, TypescriptPluginData } from "@documentalist/client"; -import { readFileSync } from "fs"; -import { dirname } from "path"; +import { readFileSync } from "node:fs"; +import { dirname } from "node:path"; import { tsconfigResolverSync } from "tsconfig-resolver"; import { Application, LogLevel, TSConfigReader, TypeDocOptions, TypeDocReader } from "typedoc"; import * as ts from "typescript"; + import { Visitor } from "./visitor"; export interface TypescriptPluginOptions { @@ -208,7 +209,7 @@ export class TypescriptPlugin implements Plugin { ); } - return compiler.objectify(visitor.visitProject(project), i => i.name); + return compiler.objectify(await visitor.visitProject(project), i => i.name); } private resolveClosestTsconfig(file: File) { diff --git a/packages/compiler/src/plugins/typescript/visitor.ts b/packages/compiler/src/plugins/typescript/visitor.ts index 6d2abcf..24ca15d 100644 --- a/packages/compiler/src/plugins/typescript/visitor.ts +++ b/packages/compiler/src/plugins/typescript/visitor.ts @@ -31,7 +31,7 @@ import { TsSignature, TsTypeAlias, } from "@documentalist/client"; -import { relative } from "path"; +import { relative } from "node:path"; import { Comment, DeclarationReflection, @@ -41,6 +41,7 @@ import { ReflectionKind, SignatureReflection, } from "typedoc"; + import { TypescriptPluginOptions } from "./typescriptPlugin"; import { resolveSignature, resolveTypeString } from "./typestring"; @@ -50,25 +51,28 @@ export class Visitor { private options: TypescriptPluginOptions, ) {} - public visitProject(project: ProjectReflection) { + public async visitProject(project: ProjectReflection) { const { excludePaths = [] } = this.options; // get top-level members of typedoc project return [ - ...this.visitChildren(project.getReflectionsByKind(ReflectionKind.Class), this.visitClass), - ...this.visitChildren(project.getReflectionsByKind(ReflectionKind.Enum), this.visitEnum), - ...this.visitChildren(project.getReflectionsByKind(ReflectionKind.Function), this.visitMethod), - ...this.visitChildren(project.getReflectionsByKind(ReflectionKind.Interface), this.visitInterface), - ...this.visitChildren(project.getReflectionsByKind(ReflectionKind.TypeAlias), def => ({ - ...this.makeDocEntry(def, Kind.TypeAlias), - type: resolveTypeString(def.type), - })), + ...(await this.visitChildren(project.getReflectionsByKind(ReflectionKind.Class), this.visitClass)), + ...(await this.visitChildren(project.getReflectionsByKind(ReflectionKind.Enum), this.visitEnum)), + ...(await this.visitChildren(project.getReflectionsByKind(ReflectionKind.Function), this.visitMethod)), + ...(await this.visitChildren(project.getReflectionsByKind(ReflectionKind.Interface), this.visitInterface)), + ...(await this.visitChildren( + project.getReflectionsByKind(ReflectionKind.TypeAlias), + async def => ({ + ...(await this.makeDocEntry(def, Kind.TypeAlias)), + type: resolveTypeString(def.type), + }), + )), ].filter( // remove members excluded by path option ref => isNotExcluded(excludePaths, ref.fileName), ); } - private makeDocEntry(ref: Reflection, kind: K): TsDocBase { + private async makeDocEntry(ref: Reflection, kind: K): Promise> { let comment = ref.comment; if (comment === undefined && ref.isDeclaration()) { @@ -84,7 +88,7 @@ export class Visitor { } return { - documentation: this.renderComment(comment), + documentation: await this.renderComment(comment), fileName: getSourceFileName(ref), flags: getFlags(ref), kind, @@ -93,71 +97,74 @@ export class Visitor { }; } - private visitClass = (def: DeclarationReflection): TsClass => ({ - ...this.visitInterface(def), - accessors: this.visitChildren(def.getChildrenByKind(ReflectionKind.Accessor), this.visitAccessor), - constructorType: this.visitChildren( - def.getChildrenByKind(ReflectionKind.Constructor), - this.visitConstructor, + private visitClass = async (def: DeclarationReflection): Promise => ({ + ...(await this.visitInterface(def)), + accessors: await this.visitChildren(def.getChildrenByKind(ReflectionKind.Accessor), this.visitAccessor), + constructorType: ( + await this.visitChildren(def.getChildrenByKind(ReflectionKind.Constructor), this.visitConstructor) )[0], kind: Kind.Class, }); - private visitInterface = (def: DeclarationReflection): TsInterface => ({ - ...this.makeDocEntry(def, Kind.Interface), + private visitInterface = async (def: DeclarationReflection): Promise => ({ + ...(await this.makeDocEntry(def, Kind.Interface)), extends: def.extendedTypes?.map(resolveTypeString), implements: def.implementedTypes?.map(resolveTypeString), - indexSignature: def.indexSignature && this.visTsignature(def.indexSignature), - methods: this.visitChildren(def.getChildrenByKind(ReflectionKind.Method), this.visitMethod, sortStaticFirst), - properties: this.visitChildren( + indexSignature: def.indexSignature && (await this.visitSignature(def.indexSignature)), + methods: await this.visitChildren( + def.getChildrenByKind(ReflectionKind.Method), + this.visitMethod, + sortStaticFirst, + ), + properties: await this.visitChildren( def.getChildrenByKind(ReflectionKind.Property), this.visitProperty, sortStaticFirst, ), }); - private visitConstructor = (def: DeclarationReflection): TsConstructor => ({ - ...this.visitMethod(def), + private visitConstructor = async (def: DeclarationReflection): Promise => ({ + ...(await this.visitMethod(def)), kind: Kind.Constructor, }); - private visitEnum = (def: DeclarationReflection): TsEnum => ({ - ...this.makeDocEntry(def, Kind.Enum), - members: this.visitChildren(def.getChildrenByKind(ReflectionKind.EnumMember), m => ({ - ...this.makeDocEntry(m, Kind.EnumMember), + private visitEnum = async (def: DeclarationReflection): Promise => ({ + ...(await this.makeDocEntry(def, Kind.Enum)), + members: await this.visitChildren(def.getChildrenByKind(ReflectionKind.EnumMember), async m => ({ + ...(await this.makeDocEntry(m, Kind.EnumMember)), defaultValue: getDefaultValue(m), })), }); - private visitProperty = (def: DeclarationReflection): TsProperty => ({ - ...this.makeDocEntry(def, Kind.Property), + private visitProperty = async (def: DeclarationReflection): Promise => ({ + ...(await this.makeDocEntry(def, Kind.Property)), defaultValue: getDefaultValue(def), inheritedFrom: def.inheritedFrom && resolveTypeString(def.inheritedFrom), type: resolveTypeString(def.type), }); - private visitMethod = (def: DeclarationReflection): TsMethod => ({ - ...this.makeDocEntry(def, Kind.Method), + private visitMethod = async (def: DeclarationReflection): Promise => ({ + ...(await this.makeDocEntry(def, Kind.Method)), inheritedFrom: def.inheritedFrom && resolveTypeString(def.inheritedFrom), - signatures: def.signatures !== undefined ? def.signatures.map(sig => this.visTsignature(sig)) : [], + signatures: await Promise.all((def.signatures ?? []).map(sig => this.visitSignature(sig))), }); - private visTsignature = (sig: SignatureReflection): TsSignature => ({ - ...this.makeDocEntry(sig, Kind.Signature), + private visitSignature = async (sig: SignatureReflection): Promise => ({ + ...(await this.makeDocEntry(sig, Kind.Signature)), flags: undefined, - parameters: (sig.parameters || []).map(param => this.visitParameter(param)), + parameters: await Promise.all((sig.parameters || []).map(param => this.visitParameter(param))), returnType: resolveTypeString(sig.type), type: resolveSignature(sig), }); - private visitParameter = (param: ParameterReflection): TsParameter => ({ - ...this.makeDocEntry(param, Kind.Parameter), + private visitParameter = async (param: ParameterReflection): Promise => ({ + ...(await this.makeDocEntry(param, Kind.Parameter)), defaultValue: getDefaultValue(param), sourceUrl: undefined, type: resolveTypeString(param.type), }); - private visitAccessor = (param: DeclarationReflection): TsAccessor => { + private visitAccessor = async (param: DeclarationReflection): Promise => { let type: string; let getDocumentation; let setDocumentation; @@ -171,29 +178,29 @@ export class Visitor { } if (param.getSignature) { - getDocumentation = this.renderComment(param.getSignature.comment); + getDocumentation = await this.renderComment(param.getSignature.comment); } if (param.setSignature) { - setDocumentation = this.renderComment(param.setSignature.comment); + setDocumentation = await this.renderComment(param.setSignature.comment); } return { - ...this.makeDocEntry(param, Kind.Accessor), + ...(await this.makeDocEntry(param, Kind.Accessor)), getDocumentation, setDocumentation, type, }; }; - /** VisTs each child that passes the filter condition (based on options). */ - private visitChildren( + /** Visits each child that passes the filter condition (based on options). */ + private async visitChildren( children: Reflection[], - visitor: (def: DeclarationReflection) => T, + visitor: (def: DeclarationReflection) => T | Promise, comparator?: (a: T, b: T) => number, - ): T[] { + ): Promise { const { excludeNames = [], excludePaths = [] } = this.options; - return children - .map(visitor) + const visited = await Promise.all(children.map(visitor)); + return visited .filter(doc => isNotExcluded(excludeNames, doc.name) && isNotExcluded(excludePaths, doc.fileName)) .sort(comparator); } diff --git a/yarn.lock b/yarn.lock index 2ae9326..dca147f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4560,10 +4560,10 @@ markdown-it@^12.0.2: mdurl "^1.0.1" uc.micro "^1.0.5" -marked@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/marked/-/marked-10.0.0.tgz#7fe1805bb908433d760e2de0fcc8841a2b2d745c" - integrity sha512-YiGcYcWj50YrwBgNzFoYhQ1hT6GmQbFG8SksnYJX1z4BXTHSOrz1GB5/Jm2yQvMg4nN1FHP4M6r03R10KrVUiA== +marked@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/marked/-/marked-11.1.1.tgz#e1b2407241f744fb1935fac224680874d9aff7a3" + integrity sha512-EgxRjgK9axsQuUa/oKMx5DEY8oXpKJfk61rT5iY3aRlgU6QJtUcxU5OAymdhCvWvhYcd9FKmO5eQoX8m9VGJXg== marked@^4.3.0: version "4.3.0"