Skip to content

Commit

Permalink
Make outputs option work properly
Browse files Browse the repository at this point in the history
  • Loading branch information
Gerrit0 committed Oct 1, 2023
1 parent e90a4d0 commit 3cca1dc
Show file tree
Hide file tree
Showing 17 changed files with 170 additions and 343 deletions.
13 changes: 11 additions & 2 deletions .config/typedoc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"$schema": "https://typedoc.org/schema.json",
"intentionallyNotExported": [
"SORT_STRATEGIES",
"_ModelToObject",
Expand Down Expand Up @@ -39,5 +38,15 @@
"includeGroups": false
},
"includeVersion": true,
"logLevel": "Verbose"
"logLevel": "Verbose",
"outputs": [
{
"type": "html",
"path": "../docs"
},
{
"type": "json",
"path": "../docs/docs.json"
}
]
}
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
### TODO

- Fix not exported warnings when building docs
- Fix options reading to correctly handle `outputs.path`

### Breaking Changes

- Introduced a new `outputs` option which can be set to an array of outputs to be produced by TypeDoc.
This can be used to render output with multiple themes, or even multiple output types.

### API Breaking Changes

Expand All @@ -12,6 +16,8 @@
Most notably, `listenTo` no longer exists. Plugins should instead use `on`.
- `Converter.EVENT_CREATE_DECLARATION` will no longer sometimes be emitted for the root level `ProjectReflection`
- Removed `IndexEvent` which was created for plugin use, but ended up not actually being necessary.
- Removed `minValue` and `maxValue` on numeric options as it is not used by any actively maintained plugins or TypeDoc.
- Removed deprecated `BindOption` decorator.

# Unreleased

Expand Down
18 changes: 16 additions & 2 deletions scripts/generate_options_schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const schema = {
};

addTypeDocOptions({
addOutputShortcut() {},
/** @param {import("../src").DeclarationOption} option */
addDeclaration(option) {
if (IGNORED_OPTIONS.has(option.name)) return;
Expand Down Expand Up @@ -64,8 +65,6 @@ addTypeDocOptions({
);
data.type = "number";
data.default = decl.defaultValue ?? 0;
data.maximum = decl.maxValue;
data.minimum = decl.minValue;
break;
}
case ParameterType.Map: {
Expand Down Expand Up @@ -157,6 +156,21 @@ delete schema.properties.sort.items.type;
schema.properties.sort.items.enum =
require("../src/lib/utils/sort").SORT_STRATEGIES;

schema.properties.outputs.type = "array";
schema.properties.outputs.items = {
type: "object",
required: ["path", "type"],
additionalProperties: false,
properties: {
type: {
anyOf: [{ enum: ["html", "json"] }, { type: "string" }],
},
path: {
type: "string",
},
},
};

const output = JSON.stringify(schema, null, "\t");

if (process.argv.length > 2) {
Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export {
export {
ArgumentsReader,
Option,
BindOption,
CommentStyle,
JSX,
LogLevel,
Expand Down
30 changes: 19 additions & 11 deletions src/lib/output/html-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { EventHooks, Option } from "../utils";
import { setRenderSettings } from "../utils/jsx";
import type { JsxElement } from "../utils/jsx.elements";
import type { MarkdownEvent } from "./events";
import { MinimalDocument, Output, Router } from "./output";
import { MinimalDocument, Output } from "./output";
import type { DefaultThemeRenderContext } from "./themes/default/DefaultThemeRenderContext";

export interface NavigationElement {
Expand Down Expand Up @@ -113,23 +113,19 @@ const kindMappings = new Map([

const URL_PREFIX = /^(http|ftp)s?:\/\//;

export abstract class HtmlOutputRouter extends Router<HtmlOutputDocument> {
export abstract class HtmlOutputRouter {
private absoluteToRelativePathMap = new Map<string, string>();
private renderStartTime = Date.now();
private location = "";
currentDocument!: HtmlOutputDocument;

@Option("cacheBust")
accessor cacheBust!: boolean;

constructor(
basePath: string,
readonly application: Application,
) {
super(basePath);
}
constructor(readonly application: Application) {}

override setCurrentDocument(doc: HtmlOutputDocument): void {
super.setCurrentDocument(doc);
setCurrentDocument(doc: HtmlOutputDocument): void {
this.currentDocument = doc;
this.location = posix.dirname(doc.filename);
}

Expand All @@ -152,6 +148,8 @@ export abstract class HtmlOutputRouter extends Router<HtmlOutputDocument> {
this.absoluteToRelativePathMap.set(key, path);
return path;
}

abstract getDocuments(project: ProjectReflection): HtmlOutputDocument[];
}

export class KindFolderHtmlOutputRouter extends HtmlOutputRouter {
Expand Down Expand Up @@ -274,6 +272,11 @@ export abstract class HtmlOutput<
> extends Output<HtmlOutputDocument, TEvents & HtmlOutputEvents> {
private _navigationCache: NavigationElement[] | undefined;

/**
* Set during rendering, but not during setup.
*/
public router!: HtmlOutputRouter;

@Option("cacheBust")
accessor cacheBust!: boolean;

Expand Down Expand Up @@ -319,7 +322,7 @@ export abstract class HtmlOutput<
*/
abstract buildNavigation(project: ProjectReflection): NavigationElement[];

abstract override buildRouter(basePath: string): HtmlOutputRouter;
abstract buildRouter(): HtmlOutputRouter;

/**
* By default, enables syntax highlighting.
Expand All @@ -328,6 +331,11 @@ export abstract class HtmlOutput<
setRenderSettings({ pretty: app.options.getValue("pretty") });
await app.getPlugin("typedoc:marked").loadHighlighter();
}

override getDocuments(project: ProjectReflection): HtmlOutputDocument[] {
this.router = this.buildRouter();
return this.router.getDocuments(project);
}
}

function hasReadme(readme: string) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/output/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { PageEvent, RendererEvent, MarkdownEvent } from "./events";
export { Renderer } from "./renderer";
export { Output, Router, type MinimalDocument } from "./output";
export { Output, type MinimalDocument } from "./output";
export { DefaultThemeRenderContext } from "./themes/default/DefaultThemeRenderContext";
31 changes: 12 additions & 19 deletions src/lib/output/json-output.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,25 @@
import { MinimalDocument, Output, Router } from "./output";
import { MinimalDocument, Output } from "./output";
import type { ProjectReflection } from "../models";
import type { Application } from "../application";

export interface JsonOutputDocument extends MinimalDocument {
model: ProjectReflection;
}

// It's somewhat silly to have a router for one document, but requiring one
// makes the renderer simpler.
export class JsonOutputRouter extends Router<JsonOutputDocument> {
override getDocuments(project: ProjectReflection) {
return [{ filename: "", model: project }];
}
}

export class JsonOutput extends Output<MinimalDocument, {}> {
export class JsonOutput extends Output<
MinimalDocument & { project: ProjectReflection },
{}
> {
constructor(private app: Application) {
super();
}

override buildRouter(basePath: string): JsonOutputRouter {
return new JsonOutputRouter(basePath);
override getDocuments(project: ProjectReflection) {
return [{ filename: "", project }];
}

override render(document: JsonOutputDocument): string | Promise<string> {
override render(document: {
project: ProjectReflection;
}): string | Promise<string> {
const json = this.app.serializer.projectToObject(
document.model,
document.filename,
document.project,
process.cwd(),
);
const pretty = this.app.options.getValue("pretty");
return JSON.stringify(json, null, pretty ? "\t" : "");
Expand Down
35 changes: 4 additions & 31 deletions src/lib/output/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,6 @@ export interface MinimalDocument {
filename: string;
}

/**
* The Router class of an Output determines which files the output
* will write and the relations between those documents. Not that this
* interface doesn't actually require a `urlTo` method because even single
* file outputs that don't have any links have a minimal router.
*/
export abstract class Router<TDocument extends MinimalDocument> {
/**
* Will not be set when {@link getDocuments} is called, but will be set
* before any url resolution methods are called.
*/
currentDocument!: TDocument;

constructor(readonly basePath: string) {}

abstract getDocuments(project: ProjectReflection): TDocument[];

setCurrentDocument(doc: TDocument) {
this.currentDocument = doc;
}
}

/**
* Base class of all output types.
*
Expand All @@ -48,11 +26,6 @@ export abstract class Output<
TDocument extends MinimalDocument,
TEvents extends Record<keyof TEvents, unknown[]> = {},
> extends EventDispatcher<TEvents> {
/**
* Will be set to the result of {@link buildRouter}
*/
public router!: ReturnType<(typeof this)["buildRouter"]>;

/**
* Will be called once before any calls to {@link render}.
*/
Expand All @@ -64,14 +37,14 @@ export abstract class Output<
async teardown(_app: Application): Promise<void> {}

/**
* Called once after {@link setup} to get the router which will be used to get the
* documents to render.
* Called once after {@link setup} to get the documents which should be passed to {@link render}.
* The filenames of all returned documents should be
*/
abstract buildRouter(basePath: string): Router<TDocument>;
abstract getDocuments(project: ProjectReflection): TDocument[];

/**
* Renders the provided page to a string, which will be written to disk by the {@link Renderer}
* This will be called for each document rendered by {@link Router.getDocuments}.
* This will be called for each document returned by {@link getDocuments}.
*/
abstract render(document: TDocument): string | Promise<string>;
}
67 changes: 16 additions & 51 deletions src/lib/output/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,43 +128,9 @@ export class Renderer extends EventDispatcher<RendererEvents> {
* Render the given project reflection to all user configured outputs.
*/
async writeOutputs(project: ProjectReflection): Promise<void> {
const options = this.application.options;
const outputs: OutputOptions[] = [];

// If the user set a shortcut option, ignore the outputs config, they probably
// just wanted this one. It'd be nice to make this available to the markdown plugin
// too...
if (options.isSet("out") || options.isSet("json")) {
if (options.getValue("out")) {
outputs.push({
type: "html",
path: options.getValue("out"),
});
}
if (options.getValue("json")) {
outputs.push({
type: "json",
path: options.getValue("json"),
});
}

if (options.isSet("outputs")) {
this.logger.info(
"Ignoring 'outputs' configuration as 'out' or 'json' was specified.",
);
}
} else if (options.isSet("outputs")) {
outputs.push(...options.getValue("outputs"));
}

// No outputs = render html to docs in current directory
if (!outputs.length) {
outputs.push({
type: "html",
path: process.cwd() + "/docs",
});
}

// This will get the "outputs" option if configured, or derive it from
// the --out / --json options otherwise.
const outputs = this.application.options.getValue("outputs");
for (const output of outputs) {
await this.writeOutput(project, output);
}
Expand All @@ -177,26 +143,27 @@ export class Renderer extends EventDispatcher<RendererEvents> {
project: ProjectReflection,
output: OutputOptions,
): Promise<void> {
const start = Date.now();
const event = new RendererEvent(output.path, project);

this.trigger(RendererEvent.BEGIN, event);
await this.runPreRenderJobs(event);

const ctor = this.outputs.get(output.type);
if (!ctor) {
this.application.logger.error(
`Skipping output "${output.type}" as it has not been defined. Ensure you have loaded the providing plugin.`,
`Skipping output "${
output.type
}" as it has not been defined. Ensure you have loaded the providing plugin. The available output types are:\n\t${Array.from(
this.outputs.keys(),
).join("\n\t")}`,
);
return;
}

const start = Date.now();
const event = new RendererEvent(output.path, project);

this.trigger(RendererEvent.BEGIN, event);
await this.runPreRenderJobs(event);

this.output = new ctor(this.application);
await this.output.setup(this.application);
const router = (this.output.router = this.output.buildRouter(
output.path,
));
const documents = router.getDocuments(project);
const documents = this.output.getDocuments(project);

if (documents.length > 1) {
// We're writing more than one document, so the output path should be a directory.
Expand All @@ -208,11 +175,9 @@ export class Renderer extends EventDispatcher<RendererEvents> {
}
}

this.logger.verbose(`There are ${documents.length} documents to write`);
this.logger.verbose(`${documents.length} document(s) to write`);

for (const doc of documents) {
router.setCurrentDocument(doc);

const pageEvent = new PageEvent(project, doc);
this.trigger(PageEvent.BEGIN, pageEvent);
pageEvent.contents = await this.output.render(doc);
Expand Down
4 changes: 2 additions & 2 deletions src/lib/output/themes/default/DefaultTheme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export class DefaultHtmlOutput<TEvents extends Record<keyof TEvents, unknown[]>>
@Option("visibilityFilters")
accessor visibilityFilters!: Record<string, boolean>;

override buildRouter(basePath: string): HtmlOutputRouter {
return new KindFolderHtmlOutputRouter(basePath, this.application);
override buildRouter(): HtmlOutputRouter {
return new KindFolderHtmlOutputRouter(this.application);
}

getRenderContext(doc: HtmlOutputDocument): DefaultThemeRenderContext {
Expand Down
Loading

0 comments on commit 3cca1dc

Please sign in to comment.