Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support for export declarations #1157

Merged
merged 19 commits into from Jan 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/basic/src/mod.ts
@@ -0,0 +1,6 @@
export const a = 1;

/**
* Will not be included in mod2
*/
export default function() {}
6 changes: 6 additions & 0 deletions examples/basic/src/mod2.ts
@@ -0,0 +1,6 @@
export * from './mod';

/**
* Will be exported from mod2, unlike the default function in mod
*/
export default function () {}
20 changes: 10 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
@@ -1,7 +1,7 @@
{
"name": "typedoc",
"description": "Create api documentation for TypeScript projects.",
"version": "0.15.6",
"version": "0.16.0-6",
"homepage": "https://typedoc.org",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down Expand Up @@ -32,14 +32,14 @@
"dependencies": {
"@types/minimatch": "3.0.3",
"fs-extra": "^8.1.0",
"handlebars": "^4.5.3",
"handlebars": "^4.7.0",
"highlight.js": "^9.17.1",
"lodash": "^4.17.15",
"marked": "^0.8.0",
"minimatch": "^3.0.0",
"progress": "^2.0.3",
"shelljs": "^0.8.3",
"typedoc-default-themes": "^0.6.3",
"typedoc-default-themes": "0.7.0-2",
"typescript": "3.7.x"
},
"devDependencies": {
Expand Down
9 changes: 8 additions & 1 deletion scripts/rebuild_specs.js
Expand Up @@ -17,7 +17,8 @@ const app = new TypeDoc.Application({
"lib.es2015.iterable.d.ts",
"lib.es2015.collection.d.ts"
],
name: 'typedoc'
name: 'typedoc',
excludeExternals: true
});

// Note that this uses the test files in dist, not in src, this is important since
Expand All @@ -27,6 +28,10 @@ const base = path.join(__dirname, '../dist/test/converter');
/** @type {[string, () => void, () => void][]} */
const conversions = [
['specs', () => { }, () => { }],
['specs.d',
() => app.options.setValue('includeDeclarations', true),
() => app.options.setValue('includeDeclarations', false)
],
['specs-without-exported',
() => app.options.setValue('excludeNotExported', true),
() => app.options.setValue('excludeNotExported', false)
Expand Down Expand Up @@ -73,7 +78,9 @@ async function rebuildRendererTest() {
const out = path.join(__dirname, '../src/test/renderer/specs');

await fs.remove(out)
app.options.setValue('excludeExternals', false);
app.generateDocs(app.expandInputFiles([src]), out)
app.options.setValue('excludeExternals', true);
await fs.remove(path.join(out, 'assets'))

/**
Expand Down
15 changes: 15 additions & 0 deletions src/lib/converter/context.ts
Expand Up @@ -151,6 +151,14 @@ export class Context {
return nodeType;
}

getSymbolAtLocation(node: ts.Node): ts.Symbol | undefined {
let symbol = this.checker.getSymbolAtLocation(node);
if (!symbol && isNamedNode(node)) {
symbol = this.checker.getSymbolAtLocation(node.name);
}
return symbol;
}

/**
* Return the current logger instance.
*
Expand Down Expand Up @@ -390,3 +398,10 @@ export class Context {
return typeParameters;
}
}

function isNamedNode(node: ts.Node): node is ts.Node & { name: ts.Identifier | ts.ComputedPropertyName } {
return node['name'] && (
ts.isIdentifier(node['name']) ||
ts.isComputedPropertyName(node['name'])
);
}
5 changes: 3 additions & 2 deletions src/lib/converter/convert-expression.ts
Expand Up @@ -26,8 +26,9 @@ export function convertExpression(expression: ts.Expression): string {
return 'true';
case ts.SyntaxKind.FalseKeyword:
return 'false';
case ts.SyntaxKind.NullKeyword:
return 'null';
default:
const source = expression.getSourceFile();
return source.text.substring(expression.pos, expression.end);
return expression.getText(expression.getSourceFile());
}
}
10 changes: 10 additions & 0 deletions src/lib/converter/converter.ts
Expand Up @@ -285,6 +285,16 @@ export class Converter extends ChildableComponent<Application, ConverterComponen
const errors = this.compile(context);
const project = this.resolve(context);

const dangling = project.getDanglingReferences();
if (dangling.length) {
this.owner.logger.warn([
'Some names refer to reflections that do not exist.',
'This can be caused by exporting a reflection only under a different name than it is declared',
'or by a plugin removing reflections. The names that cannot be resolved are:',
...dangling
].join('\n'));
}

this.trigger(Converter.EVENT_END, context);

return {
Expand Down
34 changes: 23 additions & 11 deletions src/lib/converter/factories/declaration.ts
Expand Up @@ -71,21 +71,33 @@ export function createDeclaration(context: Context, node: ts.Declaration, kind:

// Test whether the node is exported
let isExported: boolean;
if (container.kindOf([ReflectionKind.Module, ReflectionKind.ExternalModule])) {
isExported = false; // Don't inherit exported state in modules and namespaces
if (kind === ReflectionKind.ExternalModule || kind === ReflectionKind.Global) {
isExported = true;
} else if (container.kind === ReflectionKind.Global) {
// In file mode, everything is exported.
isExported = true;
} else if (container.kindOf([ReflectionKind.Module, ReflectionKind.ExternalModule])) {
const symbol = context.getSymbolAtLocation(node);
if (!symbol) {
isExported = false;
} else {
let parentNode = node.parent;
while (![ts.SyntaxKind.SourceFile, ts.SyntaxKind.ModuleDeclaration].includes(parentNode.kind)) {
parentNode = parentNode.parent;
}
const parentSymbol = context.getSymbolAtLocation(parentNode);
if (!parentSymbol) {
// This is a file with no imports/exports, so everything is
// global and therefore exported.
isExported = true;
} else {
isExported = !!parentSymbol.exports?.get(symbol.escapedName);
}
}
} else {
isExported = container.flags.isExported;
}

if (kind === ReflectionKind.ExternalModule) {
isExported = true; // Always mark external modules as exported
} else if (node.parent && node.parent.kind === ts.SyntaxKind.VariableDeclarationList) {
const parentModifiers = ts.getCombinedModifierFlags(node.parent.parent as ts.Declaration);
isExported = isExported || !!(parentModifiers & ts.ModifierFlags.Export);
} else {
isExported = isExported || !!(modifiers & ts.ModifierFlags.Export);
}

if (!isExported && context.converter.excludeNotExported) {
return;
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/converter/factories/parameter.ts
Expand Up @@ -23,6 +23,7 @@ export function createParameter(context: Context, node: ts.ParameterDeclaration)
}

const parameter = new ParameterReflection(node.symbol.name, ReflectionKind.Parameter, signature);
parameter.flags.setFlag(ReflectionFlag.Exported, context.scope.flags.isExported);
context.registerReflection(parameter, node);
context.withScope(parameter, () => {
if (ts.isArrayBindingPattern(node.name) || ts.isObjectBindingPattern(node.name)) {
Expand Down
21 changes: 20 additions & 1 deletion src/lib/converter/factories/reference.ts
@@ -1,7 +1,9 @@
import * as ts from 'typescript';

import { ReferenceType } from '../../models/types/index';
import { ReferenceType, ReferenceReflection, ContainerReflection, ReflectionFlag } from '../../models';
import { Context } from '../context';
import { ReferenceState } from '../../models/reflections/reference';
import { Converter } from '../converter';

/**
* Create a new reference type pointing to the given symbol.
Expand All @@ -26,3 +28,20 @@ export function createReferenceType(context: Context, symbol: ts.Symbol | undefi

return new ReferenceType(name, id);
}

export function createReferenceReflection(context: Context, source: ts.Symbol, target: ts.Symbol): ReferenceReflection {
if (!(context.scope instanceof ContainerReflection)) {
throw new Error('Cannot add reference to a non-container');
}

const reflection = new ReferenceReflection(source.name, [ReferenceState.Unresolved, context.getSymbolID(target)!], context.scope);
reflection.flags.setFlag(ReflectionFlag.Exported, true); // References are exported by necessity
if (!context.scope.children) {
context.scope.children = [];
}
context.scope.children.push(reflection);
context.registerReflection(reflection, undefined, source);
context.trigger(Converter.EVENT_CREATE_DECLARATION, reflection);

return reflection;
}
3 changes: 2 additions & 1 deletion src/lib/converter/factories/signature.ts
@@ -1,6 +1,6 @@
import * as ts from 'typescript';

import { ReflectionKind, SignatureReflection, ContainerReflection, DeclarationReflection, Type } from '../../models/index';
import { ReflectionKind, SignatureReflection, ContainerReflection, DeclarationReflection, Type, ReflectionFlag } from '../../models/index';
import { Context } from '../context';
import { Converter } from '../converter';
import { createParameter } from './parameter';
Expand All @@ -22,6 +22,7 @@ export function createSignature(context: Context, node: ts.SignatureDeclaration,
}

const signature = new SignatureReflection(name, kind, container);
signature.flags.setFlag(ReflectionFlag.Exported, container.flags.isExported);
context.registerReflection(signature, node);
context.withScope(signature, node.typeParameters, true, () => {
node.parameters.forEach((parameter: ts.ParameterDeclaration) => {
Expand Down
3 changes: 2 additions & 1 deletion src/lib/converter/factories/type-parameter.ts
@@ -1,6 +1,6 @@
import * as ts from 'typescript';

import { TypeParameterContainer, TypeParameterReflection, TypeParameterType } from '../../models/index';
import { TypeParameterContainer, TypeParameterReflection, TypeParameterType, ReflectionFlag } from '../../models/index';
import { Context } from '../context';
import { Converter } from '../converter';

Expand All @@ -23,6 +23,7 @@ export function createTypeParameter(context: Context, node: ts.TypeParameterDecl

const reflection = <TypeParameterContainer> context.scope;
const typeParameterReflection = new TypeParameterReflection(typeParameter, reflection);
typeParameterReflection.flags.setFlag(ReflectionFlag.Exported, reflection.flags.isExported);

if (!reflection.typeParameters) {
reflection.typeParameters = [];
Expand Down
48 changes: 47 additions & 1 deletion src/lib/converter/nodes/export.ts
@@ -1,9 +1,11 @@
import * as ts from 'typescript';

import { Reflection, ReflectionFlag, DeclarationReflection } from '../../models/index';
import { Reflection, ReflectionFlag, DeclarationReflection, ContainerReflection } from '../../models/index';
import { Context } from '../context';
import { Component, ConverterNodeComponent } from '../components';
import { createReferenceReflection } from '../factories/reference';

// TODO: With 9c3114d this converter should no longer be necessary. Verify and remove.
@Component({name: 'node:export'})
export class ExportConverter extends ConverterNodeComponent<ts.ExportAssignment> {
/**
Expand Down Expand Up @@ -53,3 +55,47 @@ export class ExportConverter extends ConverterNodeComponent<ts.ExportAssignment>
return context.scope;
}
}

@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 scope = context.scope;
if (!(scope instanceof ContainerReflection)) {
throw new Error('Expected to be within a container');
}

if (node.exportClause) { // export { a, a as b }
node.exportClause.elements.forEach(specifier => {
const source = context.getSymbolAtLocation(specifier.name);
const target = context.getSymbolAtLocation(specifier.propertyName ?? specifier.name);
if (source && target) {
const original = (target.flags & ts.SymbolFlags.Alias) ? context.checker.getAliasedSymbol(target) : target;
// 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) {
return;
}

createReferenceReflection(context, source, original);
}
});
} else if (node.moduleSpecifier) { // export * from ...
const thisModule = context.getSymbolAtLocation(node.getSourceFile())!;
const sourceFileSymbol = context.getSymbolAtLocation(node.moduleSpecifier);
sourceFileSymbol?.exports?.forEach((symbol, key) => {
// Default exports are not re-exported with export * from
if (key === 'default' as ts.__String) {
return;
}
const source = context.checker.tryGetMemberInModuleExports(key.toString().replace(/^__/, '_'), thisModule);
if (source) {
createReferenceReflection(context, source, symbol);
}
});
}

return context.scope;
}
}
3 changes: 2 additions & 1 deletion src/lib/converter/types/binding-object.ts
@@ -1,6 +1,6 @@
import * as ts from 'typescript';

import { Type, ReflectionKind, DeclarationReflection, ReflectionType } from '../../models/index';
import { Type, ReflectionKind, DeclarationReflection, ReflectionType, ReflectionFlag } from '../../models/index';
import { Component, ConverterTypeComponent, TypeNodeConverter } from '../components';
import { Context } from '../context';
import { Converter } from '../converter';
Expand All @@ -23,6 +23,7 @@ export class BindingObjectConverter extends ConverterTypeComponent implements Ty
*/
convertNode(context: Context, node: ts.BindingPattern): Type {
const declaration = new DeclarationReflection('__type', ReflectionKind.TypeLiteral, context.scope);
declaration.flags.setFlag(ReflectionFlag.Exported, context.scope.flags.isExported);

context.registerReflection(declaration);
context.trigger(Converter.EVENT_CREATE_DECLARATION, declaration, node);
Expand Down
9 changes: 4 additions & 5 deletions src/lib/converter/types/reference.ts
@@ -1,7 +1,7 @@
import * as ts from 'typescript';

import { Type, IntrinsicType, ReflectionType } from '../../models/types/index';
import { ReflectionKind, DeclarationReflection } from '../../models/reflections/index';
import { ReflectionKind, DeclarationReflection, ReflectionFlag } from '../../models/reflections/index';
import { createReferenceType } from '../factories/index';
import { Component, ConverterTypeComponent, TypeNodeConverter } from '../components';
import { Context } from '../context';
Expand Down Expand Up @@ -115,16 +115,15 @@ export class ReferenceConverter extends ConverterTypeComponent implements TypeNo
if (context.visitStack.includes(declaration)) {
if (declaration.kind === ts.SyntaxKind.TypeLiteral ||
declaration.kind === ts.SyntaxKind.ObjectLiteralExpression) {
// TODO: Check if this type assertion is safe and document.
return createReferenceType(context, declaration.parent.symbol!);
return createReferenceType(context, declaration.parent.symbol);
} else {
// TODO: Check if this type assertion is safe and document.
return createReferenceType(context, declaration.symbol!);
return createReferenceType(context, declaration.symbol);
}
}
}

const declaration = new DeclarationReflection('__type', ReflectionKind.TypeLiteral, context.scope);
declaration.flags.setFlag(ReflectionFlag.Exported, context.scope.flags.isExported);

context.registerReflection(declaration, undefined, symbol);
context.trigger(Converter.EVENT_CREATE_DECLARATION, declaration, node);
Expand Down