diff --git a/.vscode/launch.json b/.vscode/launch.json index a34001678..706f34121 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,16 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Attach", + "port": 9229, + "request": "attach", + "skipFiles": [ + "/**" + ], + "type": "pwa-node", + "sourceMaps": true + }, { "type": "node", "request": "launch", diff --git a/src/lib/converter/context.ts b/src/lib/converter/context.ts index 0bba36d05..56c8160f1 100644 --- a/src/lib/converter/context.ts +++ b/src/lib/converter/context.ts @@ -245,7 +245,7 @@ export class Context { * @param node The TypeScript node containing the source file declaration. * @param callback The callback that should be executed. */ - withSourceFile(node: ts.SourceFile, callback: Function) { + withSourceFile(node: ts.SourceFile, callback: () => void) { const isExternal = this.isExternalFile(node.fileName); if (this.isOutsideDocumentation(node.fileName, isExternal)) { return; diff --git a/src/lib/converter/converter.ts b/src/lib/converter/converter.ts index cc59044a4..543876fb1 100644 --- a/src/lib/converter/converter.ts +++ b/src/lib/converter/converter.ts @@ -225,6 +225,7 @@ export class Converter extends ChildableComponent< } const project = this.resolve(context); + // This should only do anything if a plugin does something bad. project.removeDanglingReferences(); this.trigger(Converter.EVENT_END, context); @@ -254,7 +255,7 @@ export class Converter extends ChildableComponent< result = this.nodeConverters[node.kind].convert(context, node); } else { this.application.logger.warn( - `Missing converter for node having kind ${ + `Missing converter for node with kind ${ ts.SyntaxKind[node.kind] }` ); diff --git a/src/lib/converter/factories/declaration.ts b/src/lib/converter/factories/declaration.ts index 6b7a77926..65c28cf7c 100644 --- a/src/lib/converter/factories/declaration.ts +++ b/src/lib/converter/factories/declaration.ts @@ -49,10 +49,10 @@ function shouldBeIgnoredAsNotDocumented( node: ts.Declaration, kind: ReflectionKind ): boolean { - // never ignore modules, global, and enum members + // never ignore modules, the project, or enum members if ( kind === ReflectionKind.Module || - kind === ReflectionKind.Global || + kind === ReflectionKind.Project || kind === ReflectionKind.EnumMember ) { return false; diff --git a/src/lib/converter/factories/reference.ts b/src/lib/converter/factories/reference.ts index e5d337fb1..bde733b00 100644 --- a/src/lib/converter/factories/reference.ts +++ b/src/lib/converter/factories/reference.ts @@ -5,11 +5,9 @@ import { ReferenceReflection, ContainerReflection, DeclarationReflection, - ReflectionKind, } from "../../models"; import { Context } from "../context"; import { Converter } from "../converter"; -import { createDeclaration } from "./declaration"; /** * Create a new reference type pointing to the given symbol. @@ -59,7 +57,7 @@ export function createReferenceOrDeclarationReflection( return; } - let reflection: DeclarationReflection | undefined; + let reflection: DeclarationReflection | undefined = undefined; if (context.project.getReflectionFromSymbol(target)) { reflection = new ReferenceReflection( source.name, @@ -67,22 +65,19 @@ export function createReferenceOrDeclarationReflection( context.scope ); - // target === source happens when doing export * from ... - // and the original symbol is not renamed and exported from the imported module. - context.registerReflection( - reflection, - target === source ? undefined : source - ); + context.registerReflection(reflection, source); context.scope.children ??= []; context.scope.children.push(reflection); context.trigger(Converter.EVENT_CREATE_DECLARATION, reflection); - } else { - reflection = createDeclaration( + } else if (target.getDeclarations()?.[0]) { + const refl = context.converter.convertNode( context, - target.valueDeclaration, - ReflectionKind.Variable, - source.name + target.declarations[0] ); + if (refl instanceof DeclarationReflection) { + refl.name = source.name; + reflection = refl; + } } return reflection; diff --git a/src/lib/converter/nodes/block.ts b/src/lib/converter/nodes/block.ts index 0f876b879..2a815b06f 100644 --- a/src/lib/converter/nodes/block.ts +++ b/src/lib/converter/nodes/block.ts @@ -4,6 +4,9 @@ import { Reflection, ReflectionKind } from "../../models/index"; import { createDeclaration } from "../factories/index"; import { Context } from "../context"; import { Component, ConverterNodeComponent } from "../components"; +import { Converter } from ".."; +import { getCommonDirectory } from "../../utils/fs"; +import { relative, resolve } from "path"; @Component({ name: "node:block" }) export class BlockConverter extends ConverterNodeComponent< @@ -17,6 +20,20 @@ export class BlockConverter extends ConverterNodeComponent< ts.SyntaxKind.SourceFile, ]; + // Created in initialize + private entryPoints!: string[]; + private baseDir!: string; + + initialize() { + super.initialize(); + this.owner.on(Converter.EVENT_BEGIN, () => { + this.entryPoints = this.application.options + .getValue("entryPoints") + .map((path) => this.normalizeFileName(resolve(path))); + this.baseDir = getCommonDirectory(this.entryPoints); + }); + } + /** * Analyze the given class declaration node and create a suitable reflection. * @@ -27,9 +44,9 @@ export class BlockConverter extends ConverterNodeComponent< convert( context: Context, node: ts.SourceFile | ts.ModuleBlock - ): Reflection { + ): Reflection | undefined { if (node.kind === ts.SyntaxKind.SourceFile) { - this.convertSourceFile(context, node); + return this.convertSourceFile(context, node); } else { for (const exp of this.getExports(context, node)) { for (const decl of exp.getDeclarations() ?? []) { @@ -55,27 +72,42 @@ export class BlockConverter extends ConverterNodeComponent< let result: Reflection | undefined = context.scope; context.withSourceFile(node, () => { - if (context.inFirstPass) { - result = createDeclaration( - context, - node, - ReflectionKind.Module, - node.fileName - ); - context.withScope(result, () => { - this.convertExports(context, node); - }); - } else { + if (this.isEntryPoint(node.fileName)) { const symbol = context.checker.getSymbolAtLocation(node) ?? node.symbol; - if (symbol) { + if (context.inFirstPass) { + if (this.entryPoints.length === 1) { + result = context.project; + context.project.registerReflection(result, symbol); + } else { + result = createDeclaration( + context, + node, + ReflectionKind.Module, + this.getModuleName(node.fileName) + ); + } + context.withScope(result, () => { + this.convertExports(context, node); + }); + } else if (symbol) { result = context.project.getReflectionFromSymbol(symbol); context.withScope(result, () => { this.convertReExports(context, node); }); } + } else { + result = createDeclaration( + context, + node, + ReflectionKind.Module, + this.getModuleName(node.fileName) + ); + context.withScope(result, () => { + this.convertExports(context, node); + }); } }); @@ -88,8 +120,11 @@ export class BlockConverter extends ConverterNodeComponent< ) { // We really need to rebuild the converters to work on a symbol basis rather than a node // basis... this relies on us getting declaration merging right, which is dangerous at best - for (const exp of this.getExports(context, node).filter( - (exp) => context.resolveAliasedSymbol(exp) === exp + for (const exp of this.getExports(context, node).filter((exp) => + context + .resolveAliasedSymbol(exp) + .getDeclarations() + ?.every((d) => d.getSourceFile() === node.getSourceFile()) )) { for (const decl of exp.getDeclarations() ?? []) { this.owner.convertNode(context, decl); @@ -101,8 +136,11 @@ export class BlockConverter extends ConverterNodeComponent< context: Context, node: ts.SourceFile | ts.ModuleBlock ) { - for (const exp of this.getExports(context, node).filter( - (exp) => context.resolveAliasedSymbol(exp) !== exp + for (const exp of this.getExports(context, node).filter((exp) => + context + .resolveAliasedSymbol(exp) + .getDeclarations() + ?.some((d) => d.getSourceFile() !== node.getSourceFile()) )) { for (const decl of exp.getDeclarations() ?? []) { this.owner.convertNode(context, decl); @@ -134,4 +172,19 @@ export class BlockConverter extends ConverterNodeComponent< ?.some((d) => d.getSourceFile() === sourceFile) ); } + + private getModuleName(fileName: string) { + return this.normalizeFileName(relative(this.baseDir, fileName)).replace( + /\.[tj]sx?$/, + "" + ); + } + + private isEntryPoint(fileName: string) { + return this.entryPoints.includes(fileName); + } + + private normalizeFileName(fileName: string) { + return fileName.replace(/\\/g, "/"); + } } diff --git a/src/lib/converter/nodes/export.ts b/src/lib/converter/nodes/export.ts index 2c5805ba0..bd5b761c0 100644 --- a/src/lib/converter/nodes/export.ts +++ b/src/lib/converter/nodes/export.ts @@ -4,7 +4,6 @@ import { Reflection, ReflectionFlag, DeclarationReflection, - ContainerReflection, } from "../../models/index"; import { Context } from "../context"; import { Component, ConverterNodeComponent } from "../components"; @@ -55,88 +54,6 @@ export class ExportConverter extends ConverterNodeComponent< } } -@Component({ name: "node:export-declaration" }) -export class ExportDeclarationConverter extends ConverterNodeComponent< - ts.ExportDeclaration -> { - supports = [ts.SyntaxKind.ExportDeclaration]; - - convert( - context: Context, - node: ts.ExportDeclaration - ): Reflection | undefined { - const withinNamespace = node.parent.kind === ts.SyntaxKind.ModuleBlock; - - const scope = context.scope; - if (!(scope instanceof ContainerReflection)) { - throw new Error("Expected to be within a container"); - } - - if ( - node.exportClause && - node.exportClause.kind === ts.SyntaxKind.NamedExports - ) { - // export { a, a as b } - node.exportClause.elements.forEach((specifier) => { - const source = context.expectSymbolAtLocation(specifier.name); - const target = context.resolveAliasedSymbol( - context.expectSymbolAtLocation( - specifier.propertyName ?? specifier.name - ) - ); - // If the original declaration is in this file, export {} was used with something - // defined in this file and we don't need to create a reference unless the name is different. - if ( - !node.moduleSpecifier && - !specifier.propertyName && - !withinNamespace - ) { - return; - } - - createReferenceOrDeclarationReflection(context, source, target); - }); - } else if ( - node.exportClause && - node.exportClause.kind === ts.SyntaxKind.NamespaceExport - ) { - // export * as ns from ... - const source = context.expectSymbolAtLocation( - node.exportClause.name - ); - if (!node.moduleSpecifier) { - throw new Error( - "Namespace export is missing a module specifier." - ); - } - const target = context.resolveAliasedSymbol( - context.expectSymbolAtLocation(node.moduleSpecifier) - ); - createReferenceOrDeclarationReflection(context, source, target); - } else if (node.moduleSpecifier) { - // export * from ... - const sourceFileSymbol = context.expectSymbolAtLocation( - node.moduleSpecifier - ); - for (const symbol of context.checker.getExportsOfModule( - sourceFileSymbol - )) { - if (symbol.name === "default") { - // Default exports are not re-exported with export * - continue; - } - createReferenceOrDeclarationReflection( - context, - symbol, - context.resolveAliasedSymbol(symbol) - ); - } - } - - return context.scope; - } -} - @Component({ name: "node:export-specifier" }) export class ExportSpecifierConverter extends ConverterNodeComponent< ts.ExportSpecifier diff --git a/src/lib/converter/plugins/DynamicModulePlugin.ts b/src/lib/converter/plugins/DynamicModulePlugin.ts deleted file mode 100644 index cf9a0511e..000000000 --- a/src/lib/converter/plugins/DynamicModulePlugin.ts +++ /dev/null @@ -1,88 +0,0 @@ -import * as ts from "typescript"; -import * as Path from "path"; - -import { Reflection, ReflectionKind } from "../../models/reflections/abstract"; -import { Component, ConverterComponent } from "../components"; -import { BasePath } from "../utils/base-path"; -import { Converter } from "../converter"; -import { Context } from "../context"; - -/** - * A handler that truncates the names of dynamic modules to not include the - * project's base path. - */ -@Component({ name: "dynamic-module" }) -export class DynamicModulePlugin extends ConverterComponent { - /** - * Helper class for determining the base path. - */ - private basePath = new BasePath(); - - /** - * List of reflections whose name must be trimmed. - */ - private reflections!: Reflection[]; - - /** - * Create a new DynamicModuleHandler instance. - */ - initialize() { - this.listenTo(this.owner, { - [Converter.EVENT_BEGIN]: this.onBegin, - [Converter.EVENT_CREATE_DECLARATION]: this.onDeclaration, - [Converter.EVENT_RESOLVE_BEGIN]: this.onBeginResolve, - }); - } - - /** - * Triggered when the converter begins converting a project. - * - * @param context The context object describing the current state the converter is in. - */ - private onBegin(context: Context) { - this.basePath.reset(); - this.reflections = []; - if (context.getCompilerOptions().baseUrl) { - this.basePath.add( - (context.getCompilerOptions().baseUrl as string) + "/file" - ); - } - } - - /** - * Triggered when the converter has created a declaration reflection. - * - * @param context The context object describing the current state the converter is in. - * @param reflection The reflection that is currently processed. - * @param node The node that is currently processed if available. - */ - private onDeclaration( - _context: Context, - reflection: Reflection, - _node?: ts.Node - ) { - if (reflection.kindOf(ReflectionKind.Module)) { - let name = reflection.name; - if (!name.includes("/")) { - return; - } - - name = name.replace(/"/g, ""); - this.reflections.push(reflection); - this.basePath.add(name); - } - } - - /** - * Triggered when the converter begins resolving a project. - * - * @param context The context object describing the current state the converter is in. - */ - private onBeginResolve(_context: Context) { - this.reflections.forEach((reflection) => { - let name = reflection.name.replace(/"/g, ""); - name = name.substr(0, name.length - Path.extname(name).length); - reflection.name = '"' + this.basePath.trim(name) + '"'; - }); - } -} diff --git a/src/lib/converter/plugins/GroupPlugin.ts b/src/lib/converter/plugins/GroupPlugin.ts index 929d1eaf8..446ec0cb9 100644 --- a/src/lib/converter/plugins/GroupPlugin.ts +++ b/src/lib/converter/plugins/GroupPlugin.ts @@ -21,7 +21,7 @@ export class GroupPlugin extends ConverterComponent { * Define the sort order of reflections. */ static WEIGHTS = [ - ReflectionKind.Global, + ReflectionKind.Project, ReflectionKind.Module, ReflectionKind.Namespace, ReflectionKind.Enum, diff --git a/src/lib/converter/plugins/index.ts b/src/lib/converter/plugins/index.ts index 58ad7bc1d..8c88a6702 100644 --- a/src/lib/converter/plugins/index.ts +++ b/src/lib/converter/plugins/index.ts @@ -2,7 +2,6 @@ export { CategoryPlugin } from "./CategoryPlugin"; export { CommentPlugin } from "./CommentPlugin"; export { DecoratorPlugin } from "./DecoratorPlugin"; export { DeepCommentPlugin } from "./DeepCommentPlugin"; -export { DynamicModulePlugin } from "./DynamicModulePlugin"; export { GitHubPlugin } from "./GitHubPlugin"; export { GroupPlugin } from "./GroupPlugin"; export { ImplementsPlugin } from "./ImplementsPlugin"; diff --git a/src/lib/models/reflections/abstract.ts b/src/lib/models/reflections/abstract.ts index 46f0a5f7c..ec787a961 100644 --- a/src/lib/models/reflections/abstract.ts +++ b/src/lib/models/reflections/abstract.ts @@ -35,7 +35,7 @@ export function resetReflectionID() { * Defines the available reflection kinds. */ export enum ReflectionKind { - Global = 0, + Project = 0, Module = 1 << 0, Namespace = 1 << 1, Enum = 1 << 2, diff --git a/src/lib/models/reflections/project.ts b/src/lib/models/reflections/project.ts index d9a69b580..d0588c366 100644 --- a/src/lib/models/reflections/project.ts +++ b/src/lib/models/reflections/project.ts @@ -67,7 +67,7 @@ export class ProjectReflection extends ContainerReflection { * @param name The name of the project. */ constructor(name: string) { - super(name, ReflectionKind.Global); + super(name, ReflectionKind.Project); } /** diff --git a/src/lib/output/models/NavigationItem.ts b/src/lib/output/models/NavigationItem.ts index 5755b3004..c3bdcf176 100644 --- a/src/lib/output/models/NavigationItem.ts +++ b/src/lib/output/models/NavigationItem.ts @@ -54,9 +54,9 @@ export class NavigationItem { isCurrent?: boolean; /** - * Is this the navigation node for the globals page? + * Is this the navigation node for the modules page? */ - isGlobals?: boolean; + isModules?: boolean; /** * Is this navigation node one of the parents of the current page? diff --git a/src/lib/output/plugins/NavigationPlugin.ts b/src/lib/output/plugins/NavigationPlugin.ts index 5c1214ec3..c82402355 100644 --- a/src/lib/output/plugins/NavigationPlugin.ts +++ b/src/lib/output/plugins/NavigationPlugin.ts @@ -46,7 +46,7 @@ export class NavigationPlugin extends RendererComponent { (function updateItem(item: NavigationItem) { item.isCurrent = false; item.isInPath = false; - item.isVisible = item.isGlobals; + item.isVisible = item.isModules; if ( item.url === page.url || @@ -63,7 +63,7 @@ export class NavigationPlugin extends RendererComponent { currentItems.forEach((item: NavigationItem | undefined) => { item!.isCurrent = true; - let depth = item!.isGlobals ? -1 : 0; + let depth = item!.isModules ? -1 : 0; let count = 1; while (item) { item.isInPath = true; diff --git a/src/lib/output/themes/DefaultTheme.ts b/src/lib/output/themes/DefaultTheme.ts index 6fa597d15..8d4ea8df4 100644 --- a/src/lib/output/themes/DefaultTheme.ts +++ b/src/lib/output/themes/DefaultTheme.ts @@ -134,9 +134,9 @@ export class DefaultTheme extends Theme { project.url = "index.html"; urls.push(new UrlMapping("index.html", project, "reflection.hbs")); } else { - project.url = "globals.html"; + project.url = "modules.html"; urls.push( - new UrlMapping("globals.html", project, "reflection.hbs") + new UrlMapping("modules.html", project, "reflection.hbs") ); urls.push(new UrlMapping("index.html", project, "index.hbs")); } @@ -399,19 +399,19 @@ export class NavigationBuilder { /** * Build the navigation structure. * - * @param hasSeparateGlobals Has the project a separated globals.html file? - * @return The root node of the generated navigation structure. + * @param hasReadmeFile True if the project has a readme + * @returns The root node of the generated navigation structure. */ - build(hasSeparateGlobals: boolean): NavigationItem { + build(hasReadmeFile: boolean): NavigationItem { const root = new NavigationItem("Index", "index.html"); if (this.entryPoint === this.project) { - const globals = new NavigationItem( - "Globals", - hasSeparateGlobals ? "globals.html" : "index.html", + const modules = new NavigationItem( + "Modules", + hasReadmeFile ? "modules.html" : "index.html", root ); - globals.isGlobals = true; + modules.isModules = true; } const modules: DeclarationReflection[] = []; diff --git a/src/lib/utils/fs.ts b/src/lib/utils/fs.ts index d3cdf18df..3b2e3e240 100644 --- a/src/lib/utils/fs.ts +++ b/src/lib/utils/fs.ts @@ -2,6 +2,28 @@ import * as ts from "typescript"; import * as FS from "fs"; import { dirname } from "path"; +/** + * Get the longest directory path common to all files. + */ +export function getCommonDirectory(files: readonly string[]): string { + if (!files.length) { + return ""; + } + + const roots = files.map((f) => f.split(/\\|\//)); + if (roots.length === 1) { + return roots[0].slice(0, -1).join("/"); + } + + let i = 0; + + while (new Set(roots.map((part) => part[i])).size === 1) { + i++; + } + + return roots[0].slice(0, i).join("/"); +} + /** * List of known existent directories. Used to speed up [[directoryExists]]. */