diff --git a/.changeset/cold-cycles-hope.md b/.changeset/cold-cycles-hope.md new file mode 100644 index 0000000..8d502d1 --- /dev/null +++ b/.changeset/cold-cycles-hope.md @@ -0,0 +1,5 @@ +--- +"@effect/docgen": patch +--- + +add support for namespaces diff --git a/.changeset/lovely-rice-guess.md b/.changeset/lovely-rice-guess.md new file mode 100644 index 0000000..ed0621f --- /dev/null +++ b/.changeset/lovely-rice-guess.md @@ -0,0 +1,5 @@ +--- +"@effect/docgen": patch +--- + +BugFix: remove stale modules from /docs folder diff --git a/docs/modules/Domain.ts.md b/docs/modules/Domain.ts.md index 95e821d..2732896 100644 --- a/docs/modules/Domain.ts.md +++ b/docs/modules/Domain.ts.md @@ -21,6 +21,7 @@ Added in v1.0.0 - [createInterface](#createinterface) - [createMethod](#createmethod) - [createModule](#createmodule) + - [createNamespace](#createnamespace) - [createProperty](#createproperty) - [createTypeAlias](#createtypealias) - [instances](#instances) @@ -35,6 +36,7 @@ Added in v1.0.0 - [Interface (interface)](#interface-interface) - [Method (interface)](#method-interface) - [Module (interface)](#module-interface) + - [Namespace (interface)](#namespace-interface) - [Property (interface)](#property-interface) - [TypeAlias (interface)](#typealias-interface) @@ -138,12 +140,28 @@ export declare const createModule: ( functions: ReadonlyArray, typeAliases: ReadonlyArray, constants: ReadonlyArray, - exports: ReadonlyArray + exports: ReadonlyArray, + namespaces: ReadonlyArray ) => Module ``` Added in v1.0.0 +## createNamespace + +**Signature** + +```ts +export declare const createNamespace: ( + documentable: Documentable, + interfaces: ReadonlyArray, + typeAliases: ReadonlyArray, + namespaces: ReadonlyArray +) => Namespace +``` + +Added in v1.0.0 + ## createProperty **Signature** @@ -236,6 +254,16 @@ Added in v1.0.0 ## Export (interface) +These are manual exports, like: + +```ts +const _null = ... + +export { + _null as null +} +``` + **Signature** ```ts @@ -298,6 +326,22 @@ export interface Module extends Documentable { readonly typeAliases: ReadonlyArray readonly constants: ReadonlyArray readonly exports: ReadonlyArray + readonly namespaces: ReadonlyArray +} +``` + +Added in v1.0.0 + +## Namespace (interface) + +**Signature** + +```ts +export interface Namespace extends Documentable { + readonly _tag: 'Namespace' + readonly interfaces: ReadonlyArray + readonly typeAliases: ReadonlyArray + readonly namespaces: ReadonlyArray } ``` diff --git a/docs/modules/Parser.ts.md b/docs/modules/Parser.ts.md index 89ebb8c..72d5bab 100644 --- a/docs/modules/Parser.ts.md +++ b/docs/modules/Parser.ts.md @@ -20,6 +20,7 @@ Added in v1.0.0 - [parseFunctions](#parsefunctions) - [parseInterfaces](#parseinterfaces) - [parseModule](#parsemodule) + - [parseNamespaces](#parsenamespaces) - [parseTypeAliases](#parsetypealiases) --- @@ -98,6 +99,16 @@ export declare const parseModule: Effect.Effect +``` + +Added in v1.0.0 + ## parseTypeAliases **Signature** diff --git a/src/Core.ts b/src/Core.ts index d3b4e32..c96c90b 100644 --- a/src/Core.ts +++ b/src/Core.ts @@ -416,13 +416,17 @@ const getModuleMarkdownFiles = (modules: ReadonlyArray) => // ------------------------------------------------------------------------------------- const writeMarkdown = (files: ReadonlyArray) => - Effect.map(Config.Config, (config) => join(config.outDir, "**/*.ts.md")).pipe( - Effect.tap((pattern) => Effect.logDebug(`deleting ${chalk.black(pattern)}`)), - Effect.flatMap((pattern) => - Effect.flatMap(FileSystem.FileSystem, (fileSystem) => fileSystem.removeFile(pattern)) - ), - Effect.flatMap(() => writeFiles(files)) - ) + Effect.gen(function*(_) { + const config = yield* _(Config.Config) + const fileSystem = yield* _(FileSystem.FileSystem) + const pattern = join(config.outDir, "**/*.ts.md") + yield* _(Effect.logDebug(`deleting ${chalk.black(pattern)}`)) + const paths = yield* _(fileSystem.glob(pattern)) + yield* _( + Effect.forEach(paths, (path) => fileSystem.removeFile(path), { concurrency: "unbounded" }) + ) + return yield* _(writeFiles(files)) + }) const MainLayer = Logger.replace(Logger.defaultLogger, SimpleLogger).pipe( Layer.merge(ChildProcess.ChildProcessLive), diff --git a/src/Domain.ts b/src/Domain.ts index cfb8c5f..1144541 100644 --- a/src/Domain.ts +++ b/src/Domain.ts @@ -16,8 +16,15 @@ export interface Module extends Documentable { readonly typeAliases: ReadonlyArray readonly constants: ReadonlyArray readonly exports: ReadonlyArray + readonly namespaces: ReadonlyArray } +/** + * @category model + * @since 1.0.0 + */ +export type Example = string + /** * @category model * @since 1.0.0 @@ -96,6 +103,16 @@ export interface Constant extends Documentable { } /** + * These are manual exports, like: + * + * ```ts + * const _null = ... + * + * export { + * _null as null + * } + * ``` + * * @category model * @since 1.0.0 */ @@ -108,7 +125,12 @@ export interface Export extends Documentable { * @category model * @since 1.0.0 */ -export type Example = string +export interface Namespace extends Documentable { + readonly _tag: "Namespace" + readonly interfaces: ReadonlyArray + readonly typeAliases: ReadonlyArray + readonly namespaces: ReadonlyArray +} // ------------------------------------------------------------------------------------- // constructors @@ -146,7 +168,8 @@ export const createModule = ( functions: ReadonlyArray, typeAliases: ReadonlyArray, constants: ReadonlyArray, - exports: ReadonlyArray + exports: ReadonlyArray, + namespaces: ReadonlyArray ): Module => ({ ...documentable, path, @@ -155,7 +178,8 @@ export const createModule = ( functions, typeAliases, constants, - exports + exports, + namespaces }) /** @@ -266,6 +290,23 @@ export const createExport = ( signature }) +/** + * @category constructors + * @since 1.0.0 + */ +export const createNamespace = ( + documentable: Documentable, + interfaces: ReadonlyArray, + typeAliases: ReadonlyArray, + namespaces: ReadonlyArray +): Namespace => ({ + _tag: "Namespace", + ...documentable, + interfaces, + typeAliases, + namespaces +}) + /** * @category instances * @since 1.0.0 diff --git a/src/Markdown.ts b/src/Markdown.ts index 0f06252..0df6a00 100644 --- a/src/Markdown.ts +++ b/src/Markdown.ts @@ -15,6 +15,7 @@ type Printable = | Domain.Function | Domain.Interface | Domain.TypeAlias + | Domain.Namespace const bold = (s: string) => `**${s}**` @@ -34,6 +35,8 @@ const h2 = createHeader(2) const h3 = createHeader(3) +const h4 = createHeader(4) + const getSince: (v: Option.Option) => string = Option.match({ onNone: () => "", onSome: (v) => paragraph(`Added in v${v}`) @@ -91,9 +94,7 @@ const getProperty = (p: Domain.Property): string => ) const getStaticMethods = (methods: ReadonlyArray): string => - ReadonlyArray.map(methods, (method) => getStaticMethod(method) + "\n\n").join( - "" - ) + ReadonlyArray.map(methods, (method) => getStaticMethod(method) + "\n\n").join("") const getMethods = (methods: ReadonlyArray): string => ReadonlyArray.map(methods, (method) => getMethod(method) + "\n\n").join("") @@ -166,24 +167,51 @@ const fromFunction = (f: Domain.Function): string => getSince(f.since) ) -const fromInterface = (i: Domain.Interface): string => +const fromInterface = (i: Domain.Interface, indentation: number): string => paragraph( - h2(getTitle(i.name, i.deprecated, "(interface)")), + getHeaderByIndentation(indentation)(getTitle(i.name, i.deprecated, "(interface)")), getDescription(i.description), getSignature(i.signature), getExamples(i.examples), getSince(i.since) ) -const fromTypeAlias = (ta: Domain.TypeAlias): string => +const fromTypeAlias = (ta: Domain.TypeAlias, indentation: number): string => paragraph( - h2(getTitle(ta.name, ta.deprecated, "(type alias)")), + getHeaderByIndentation(indentation)(getTitle(ta.name, ta.deprecated, "(type alias)")), getDescription(ta.description), getSignature(ta.signature), getExamples(ta.examples), getSince(ta.since) ) +const getHeaderByIndentation = (indentation: number) => { + switch (indentation) { + case 0: + return h2 + case 1: + return h3 + case 2: + return h4 + } + throw new Error(`Unsupported namespace nesting: ${indentation + 1}`) +} + +const fromNamespace = (ns: Domain.Namespace, indentation: number): string => + paragraph( + paragraph( + getHeaderByIndentation(indentation)(getTitle(ns.name, ns.deprecated, "(namespace)")), + getDescription(ns.description), + getExamples(ns.examples), + getSince(ns.since) + ), + ReadonlyArray.map(ns.interfaces, (i) => fromInterface(i, indentation + 1) + "\n\n").join(""), + ReadonlyArray.map(ns.typeAliases, (typeAlias) => + fromTypeAlias(typeAlias, indentation + 1) + "\n\n").join(""), + ReadonlyArray.map(ns.namespaces, (namespace) => + fromNamespace(namespace, indentation + 1) + "\n\n").join("") + ) + /** @internal */ export const fromPrintable = (p: Printable): string => { switch (p._tag) { @@ -196,9 +224,11 @@ export const fromPrintable = (p: Printable): string => { case "Function": return fromFunction(p) case "Interface": - return fromInterface(p) + return fromInterface(p, 0) case "TypeAlias": - return fromTypeAlias(p) + return fromTypeAlias(p, 0) + case "Namespace": + return fromNamespace(p, 0) } } @@ -209,7 +239,8 @@ const getPrintables = (module: Domain.Module): ReadonlyArray => module.exports, module.functions, module.interfaces, - module.typeAliases + module.typeAliases, + module.namespaces ]) /** diff --git a/src/Parser.ts b/src/Parser.ts index 69b7993..7a34fe8 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -275,32 +275,30 @@ const parseInterfaceDeclaration = (id: ast.InterfaceDeclaration) => ) ) +const parseInterfaces_ = (interfaces: ReadonlyArray) => + pipe( + interfaces, + ReadonlyArray.filter( + every([ + (id) => id.isExported(), + (id) => + pipe( + id.getJsDocs(), + Predicate.not(flow(getJSDocText, parseComment, shouldIgnore)) + ) + ]) + ), + Effect.validateAll(parseInterfaceDeclaration), + Effect.map(ReadonlyArray.sort(byName)) + ) + /** * @category parsers * @since 1.0.0 */ -export const parseInterfaces = pipe( - Effect.map(Source, (source) => - pipe( - source.sourceFile.getInterfaces(), - ReadonlyArray.filter( - every([ - (id) => id.isExported(), - (id) => - pipe( - id.getJsDocs(), - Predicate.not(flow(getJSDocText, parseComment, shouldIgnore)) - ) - ]) - ) - )), - Effect.flatMap((interfaceDeclarations) => - pipe( - interfaceDeclarations, - Effect.validateAll(parseInterfaceDeclaration), - Effect.map(ReadonlyArray.sort(byName)) - ) - ) +export const parseInterfaces = Effect.flatMap( + Source, + (source) => parseInterfaces_(source.sourceFile.getInterfaces()) ) // ------------------------------------------------------------------------------------- @@ -509,29 +507,30 @@ const parseTypeAliasDeclaration = (ta: ast.TypeAliasDeclaration) => ) ) +const parseTypeAliases_ = (typeAliases: ReadonlyArray) => + pipe( + typeAliases, + ReadonlyArray.filter( + every([ + (alias) => alias.isExported(), + (alias) => + pipe( + alias.getJsDocs(), + Predicate.not(flow(getJSDocText, parseComment, shouldIgnore)) + ) + ]) + ), + Effect.validateAll(parseTypeAliasDeclaration), + Effect.map(ReadonlyArray.sort(byName)) + ) + /** * @category parsers * @since 1.0.0 */ -export const parseTypeAliases = pipe( - Effect.map(Source, (source) => - pipe( - source.sourceFile.getTypeAliases(), - ReadonlyArray.filter( - every([ - (alias) => alias.isExported(), - (alias) => - pipe( - alias.getJsDocs(), - Predicate.not(flow(getJSDocText, parseComment, shouldIgnore)) - ) - ]) - ) - )), - Effect.flatMap((typeAliasDeclarations) => - pipe(typeAliasDeclarations, Effect.validateAll(parseTypeAliasDeclaration)) - ), - Effect.map(ReadonlyArray.sort(byName)) +export const parseTypeAliases = Effect.flatMap( + Source, + (source) => parseTypeAliases_(source.sourceFile.getTypeAliases()) ) // ------------------------------------------------------------------------------------- @@ -650,7 +649,7 @@ const parseExportSpecifier = (es: ast.ExportSpecifier) => ) )) -const parseExportDeclaration = (ed: ast.ExportDeclaration) => +const parseNamedExports = (ed: ast.ExportDeclaration) => pipe(ed.getNamedExports(), Effect.validateAll(parseExportSpecifier)) /** @@ -660,7 +659,7 @@ const parseExportDeclaration = (ed: ast.ExportDeclaration) => export const parseExports = pipe( Effect.map(Source, (source) => source.sourceFile.getExportDeclarations()), Effect.flatMap((exportDeclarations) => - pipe(exportDeclarations, Effect.validateAll(parseExportDeclaration)) + pipe(exportDeclarations, Effect.validateAll(parseNamedExports)) ), Effect.mapBoth({ onFailure: ReadonlyArray.flatten, @@ -668,6 +667,76 @@ export const parseExports = pipe( }) ) +// ------------------------------------------------------------------------------------- +// namespaces +// ------------------------------------------------------------------------------------- + +const parseModuleDeclaration = ( + ed: ast.ModuleDeclaration +): Effect.Effect, Domain.Namespace> => + Effect.flatMap(Source, (_source) => { + const name = ed.getName() + const getInfo = pipe( + getJSDocText(ed.getJsDocs()), + getCommentInfo(name), + Effect.mapError((e) => [e]) + ) + const getInterfaces = parseInterfaces_(ed.getInterfaces()) + const getTypeAliases = parseTypeAliases_( + ed.getTypeAliases() + ) + const getNamespaces = parseNamespaces_(ed.getModules()) + return Effect.gen(function*(_) { + const info = yield* _(getInfo) + const interfaces = yield* _(getInterfaces) + const typeAliases = yield* _(getTypeAliases) + const namespaces = yield* _(getNamespaces) + return Domain.createNamespace( + Domain.createDocumentable( + name, + info.description, + info.since, + info.deprecated, + info.examples, + info.category + ), + interfaces, + typeAliases, + namespaces + ) + }) + }) + +const parseNamespaces_ = (namespaces: ReadonlyArray) => + pipe( + namespaces, + ReadonlyArray.filter( + every([ + (module) => module.isExported(), + (module) => + pipe( + module.getJsDocs(), + Predicate.not(flow(getJSDocText, parseComment, shouldIgnore)) + ) + ]) + ), + Effect.validateAll(parseModuleDeclaration), + Effect.mapBoth({ + onFailure: ReadonlyArray.flatten, + onSuccess: ReadonlyArray.sort(byName) + }) + ) + +/** + * @category parsers + * @since 1.0.0 + */ +export const parseNamespaces: Effect.Effect< + Source | Config.Config, + Array, + Array +> = Effect.flatMap(Source, (source) => parseNamespaces_(source.sourceFile.getModules())) + // ------------------------------------------------------------------------------------- // classes // ------------------------------------------------------------------------------------- @@ -978,6 +1047,7 @@ export const parseModule = pipe( Effect.bind("classes", () => parseClasses), Effect.bind("constants", () => parseConstants), Effect.bind("exports", () => parseExports), + Effect.bind("namespaces", () => parseNamespaces), Effect.map( ({ classes, @@ -986,6 +1056,7 @@ export const parseModule = pipe( exports, functions, interfaces, + namespaces, typeAliases }) => Domain.createModule( @@ -996,7 +1067,8 @@ export const parseModule = pipe( functions, typeAliases, constants, - exports + exports, + namespaces ) ) )) diff --git a/test/Domain.ts b/test/Domain.ts index b76d0a5..e3ddf1d 100644 --- a/test/Domain.ts +++ b/test/Domain.ts @@ -34,6 +34,7 @@ describe.concurrent("Domain", () => { [], [], [], + [], [] ) @@ -45,7 +46,8 @@ describe.concurrent("Domain", () => { functions: [], typeAliases: [], constants: [], - exports: [] + exports: [], + namespaces: [] }) }) @@ -155,6 +157,7 @@ describe.concurrent("Domain", () => { [], [], [], + [], [] ) @@ -166,6 +169,7 @@ describe.concurrent("Domain", () => { [], [], [], + [], [] ) diff --git a/test/Markdown.ts b/test/Markdown.ts index 3db4506..732865e 100644 --- a/test/Markdown.ts +++ b/test/Markdown.ts @@ -10,6 +10,7 @@ import { createInterface, createMethod, createModule, + createNamespace, createProperty, createTypeAlias } from "../src/Domain" @@ -126,6 +127,91 @@ const testCases = { describe.concurrent("Markdown", () => { const print = flow(_.fromPrintable, _.prettify) + it("fromNamespace", () => { + const namespace = createNamespace( + createDocumentable( + "A", + Option.none(), + Option.some("1.0.0"), + false, + [], + Option.none() + ), + [], + [ + createTypeAlias( + createDocumentable( + "B", + Option.none(), + Option.some("1.0.1"), + false, + [], + Option.none() + ), + "export type B = string" + ) + ], + [ + createNamespace( + createDocumentable( + "C", + Option.none(), + Option.some("1.0.2"), + false, + [], + Option.none() + ), + [], + [ + createTypeAlias( + createDocumentable( + "D", + Option.none(), + Option.some("1.0.3"), + false, + [], + Option.none() + ), + "export type D = number" + ) + ], + [] + ) + ] + ) + assert.strictEqual( + print(namespace), + `## A (namespace) + +Added in v1.0.0 + +### B (type alias) + +**Signature** + +\`\`\`ts +export type B = string +\`\`\` + +Added in v1.0.1 + +### C (namespace) + +Added in v1.0.2 + +#### D (type alias) + +**Signature** + +\`\`\`ts +export type D = number +\`\`\` + +Added in v1.0.3 +` + ) + }) + it("fromClass", () => { assert.strictEqual( print(testCases.class), @@ -303,7 +389,8 @@ export type A = number [testCases.function], [testCases.typeAlias], [testCases.constant], - [testCases.export] + [testCases.export], + [] ), 1 ), @@ -462,6 +549,7 @@ Added in v1.0.0 [], [], [], + [], [] ) diff --git a/test/Parser.ts b/test/Parser.ts index a43ecd9..34a565b 100644 --- a/test/Parser.ts +++ b/test/Parser.ts @@ -3,6 +3,7 @@ import chalk from "chalk" import { Effect, Exit, Option, String } from "effect" import * as ast from "ts-morph" import * as Config from "../src/Config" +import * as Domain from "../src/Domain" import * as FileSystem from "../src/FileSystem" import * as Parser from "../src/Parser" @@ -67,6 +68,254 @@ const expectSuccess = ( describe.concurrent("Parser", () => { describe.concurrent("parsers", () => { + describe.concurrent("parseNamespaces", () => { + it("should return no `Namespaces`s if the file is empty", () => { + expectSuccess("", Parser.parseNamespaces, []) + }) + + it("should return no `Namespaces`s if there are no exported namespaces", () => { + expectSuccess("namespace A {}", Parser.parseNamespaces, []) + }) + + it("should raise an error if the namespace is not well documented", () => { + expectFailure("export namespace A {}", Parser.parseNamespaces, [ + `Missing ${chalk.bold("@since")} tag in ${chalk.bold("test#A")} documentation` + ]) + }) + + const documentableA = Domain.createDocumentable( + "A", + Option.none(), + Option.some("1.0.0"), + false, + [], + Option.none() + ) + + it("should parse an empty Namespace", () => { + expectSuccess( + ` + /** + * @since 1.0.0 + */ + export namespace A {} + `, + Parser.parseNamespaces, + [ + Domain.createNamespace(documentableA, [], [], []) + ] + ) + }) + + describe.concurrent("interfaces", () => { + it("should ignore not exported interfaces", () => { + expectSuccess( + ` + /** + * @since 1.0.0 + */ + export namespace A { + interface C {} + } + `, + Parser.parseNamespaces, + [Domain.createNamespace(documentableA, [], [], [])] + ) + }) + + it("should raise an error if the interface is not well documented", () => { + expectFailure( + ` + /** + * @since 1.0.0 + */ + export namespace A { + export interface B {} + } + `, + Parser.parseNamespaces, + [`Missing ${chalk.bold("@since")} tag in ${chalk.bold("test#B")} documentation`] + ) + }) + + it("should parse an interface", () => { + const documentableB = Domain.createDocumentable( + "B", + Option.none(), + Option.some("1.0.1"), + false, + [], + Option.none() + ) + + expectSuccess( + ` + /** + * @since 1.0.0 + */ + export namespace A { + /** + * @since 1.0.1 + */ + export interface B { + readonly d: boolean + } + } + `, + Parser.parseNamespaces, + [Domain.createNamespace( + documentableA, + [Domain.createInterface( + documentableB, + `export interface B { + readonly d: boolean + }` + )], + [], + [] + )] + ) + }) + }) + + describe.concurrent("type aliases", () => { + it("should ignore not exported type alias", () => { + expectSuccess( + ` + /** + * @since 1.0.0 + */ + export namespace A { + type C = number + } + `, + Parser.parseNamespaces, + [Domain.createNamespace(documentableA, [], [], [])] + ) + }) + + it("should raise an error if the type alias is not well documented", () => { + expectFailure( + ` + /** + * @since 1.0.0 + */ + export namespace A { + export type B = string + } + `, + Parser.parseNamespaces, + [`Missing ${chalk.bold("@since")} tag in ${chalk.bold("test#B")} documentation`] + ) + }) + + it("should parse a type alias", () => { + const documentableB = Domain.createDocumentable( + "B", + Option.none(), + Option.some("1.0.1"), + false, + [], + Option.none() + ) + + expectSuccess( + ` + /** + * @since 1.0.0 + */ + export namespace A { + /** + * @since 1.0.1 + */ + export type B = string + } + `, + Parser.parseNamespaces, + [Domain.createNamespace(documentableA, [], [ + Domain.createTypeAlias(documentableB, "export type B = string") + ], [])] + ) + }) + }) + + describe.concurrent("nested namespaces", () => { + it("should ignore not exported namespaces", () => { + expectSuccess( + ` + /** + * @since 1.0.0 + */ + export namespace A { + namespace B {} + } + `, + Parser.parseNamespaces, + [Domain.createNamespace(documentableA, [], [], [])] + ) + }) + + it("should raise an error if the namespace is not well documented", () => { + expectFailure( + ` + /** + * @since 1.0.0 + */ + export namespace A { + export namespace B {} + } + `, + Parser.parseNamespaces, + [`Missing ${chalk.bold("@since")} tag in ${chalk.bold("test#B")} documentation`] + ) + }) + + it("should parse a namespace", () => { + const documentableB = Domain.createDocumentable( + "B", + Option.none(), + Option.some("1.0.1"), + false, + [], + Option.none() + ) + const documentableC = Domain.createDocumentable( + "C", + Option.none(), + Option.some("1.0.2"), + false, + [], + Option.none() + ) + + expectSuccess( + ` + /** + * @since 1.0.0 + */ + export namespace A { + /** + * @since 1.0.1 + */ + export namespace B { + /** + * @since 1.0.2 + */ + export type C = string + } + } + `, + Parser.parseNamespaces, + [Domain.createNamespace(documentableA, [], [], [ + Domain.createNamespace(documentableB, [], [ + Domain.createTypeAlias(documentableC, "export type C = string") + ], []) + ])] + ) + }) + }) + }) + describe.concurrent("parseInterfaces", () => { it("should return no `Interface`s if the file is empty", () => { expectSuccess("", Parser.parseInterfaces, []) @@ -1061,7 +1310,8 @@ export const foo = 'foo'`, signature: "export declare const foo: \"foo\"" } ], - exports: [] + exports: [], + namespaces: [] }, { enforceExamples: true } )