feat(generation): Generate + save sidebar from this repo

This commit is contained in:
Laegel
2021-03-18 17:03:56 +01:00
parent e5dadff3eb
commit 639508190a
17 changed files with 577 additions and 134 deletions

4
.gitignore vendored
View File

@@ -1 +1,3 @@
node_modules
node_modules
src/*.d.ts
src/*.js

View File

View File

@@ -1,31 +0,0 @@
const { readdirSync, statSync } = require("fs");
const path = require("path");
const { dir } = require("console");
const generateSidebar = (originPath) => {
let out = getTree(originPath)
return out;
};
const getTree = (dirPath, items = []) => {
files = readdirSync(dirPath);
files.forEach((file) => {
console.log(file);
if (statSync(dirPath + "/" + file).isDirectory()) {
const out = {
label: file.charAt(0).toUpperCase() + file.slice(1),
type: "category",
items: getTree(dirPath + "/" + file, []),
};
items.push(out);
} else {
items.push(path.join(__dirname, dirPath, "/", file));
}
});
return items;
};
module.exports = generateSidebar;

View File

@@ -1,60 +1,35 @@
const core = require("@actions/core");
const { transformDocs } = require("../main");
const generateSidebar = require("../generateSidebar");
const { readFile, writeFile } = require("fs").promises;
const { rmdir } = require("fs").promises;
const { default: generate } = require("../src/plugin");
(async () => {
try {
// Where your docs live, should be the folder containing the crates docs
const originPath = core.getInput("originPath"); // e.g. "/path/to/project/src/";
const sidebarFile = process.env["sidebarFile"];
// Where you'll save your MD files
const targetPath = core.getInput("targetPath"); // e.g. "/path/to/docusaurus/website/docs/api/js/";
const docusaurusPath = core.getInput("docusaurusPath");
/*
Where lives your sidebars config file
Doesn't have to be JSON but it's easier to change programmatically,
you may create your own saving method
*/
const sidebarPath = core.getInput("sidebarPath"); // e.g. "/path/to/docusaurus/website/sidebars.json";
await rmdir(targetPath, { recursive: true });
// rustdoc uses relative links for crate types relations
const linksRoot = core.getInput("linksRoot"); // e.g. "/docs/api/rust/";
const entryPoints = core.getInput("entryPoints").split(",");
// await Promise.all(
// entryPoints.map((entryPoint) =>
// fs.rmdir(targetPath + entryPoint, { recursive: true })
// )
// );
// const sidebarItems = (
// await Promise.all(
// entryPoints.map(async (entryPoint) => ({
// entryPoint,
// docs: await transformDocs(
// originPath + entryPoint,
// originPath,
// targetPath
// ),
// }))
// )
// ).map((item) => generateSidebar(item.docs, item.entryPoint, originPath));
const sidebarItems = generateSidebar(item.docs, item.entryPoint, originPath)
// Automatically add the sidebar items to Docusaurus sidebar file config
const sidebarContent = JSON.parse(await readFile(sidebarPath, "utf-8"));
const index = sidebarContent.docs[3].items
.map((row, index) => (row.label && row.label === "JavaScript" ? index : 0))
.reduce((accumulator, value) => accumulator + value);
sidebarContent.docs[3].items[index].items = sidebarItems; // Specify where to put the items
console.log(sidebarContent);
// writeFile(sidebarPath, JSON.stringify(sidebarContent, null, 2));
await generate(docusaurusPath, {
entryPoints: originPath + "src",
out: targetPath,
entryDocument: "index.md",
hideInPageTOC: true,
hideBreadcrumbs: true,
watch: false,
tsconfig: originPath + "tsconfig.json",
sidebar: {
sidebarFile,
},
readme: "none",
});
console.log("Tasks completed!");
} catch (error) {
core.setFailed(error.message);
}
})();
})();

View File

@@ -1,60 +1,36 @@
// const core = require("@actions/core");
const { transformDocs } = require("./main");
const generateSidebar = require("./generateSidebar");
const { readFile, writeFile, rmdir } = require("fs").promises;
const { rmdir } = require("fs").promises;
const { default: generate } = require("./src/plugin");
(async () => {
try {
// Where your docs live, should be the folder containing the crates docs
const originPath = process.env["originPath"]; // e.g. "/path/to/project/src/";
const sidebarFile = process.env["sidebarFile"];
// Where you'll save your MD files
const targetPath = process.env["targetPath"]; // e.g. "/path/to/docusaurus/website/docs/api/js/";
/*
Where lives your sidebars config file
Doesn't have to be JSON but it's easier to change programmatically,
you may create your own saving method
*/
const sidebarPath = process.env["sidebarPath"]; // e.g. "/path/to/docusaurus/website/sidebars.json";
const docusaurusPath = process.env["docusaurusPath"];
await rmdir(targetPath, { recursive: true });
// rustdoc uses relative links for crate types relations
// const linksRoot = core.getInput("linksRoot"); // e.g. "/docs/api/rust/";
// await Promise.all(
// entryPoints.map((entryPoint) =>
// rmdir(targetPath + entryPoint, { recursive: true })
// )
// );
// const sidebarItems = (
// await Promise.all(
// entryPoints.map(async (entryPoint) => ({
// entryPoint,
// docs: await transformDocs(
// originPath + entryPoint,
// originPath,
// targetPath
// ),
// }))
// )
// ).map((item) => generateSidebar(item.docs, item.entryPoint, originPath));
const sidebarItems = generateSidebar(originPath)
// Automatically add the sidebar items to Docusaurus sidebar file config
const sidebarContent = JSON.parse(await readFile(sidebarPath, "utf-8"));
const index = sidebarContent.docs[3].items
.map((row, index) => (row.label && row.label === "JavaScript" ? index : 0))
.reduce((accumulator, value) => accumulator + value);
sidebarContent.docs[3].items[index].items = sidebarItems; // Specify where to put the items
console.log(sidebarContent.docs[3].items[index].items);
// writeFile(sidebarPath, JSON.stringify(sidebarContent, null, 2));
await generate(docusaurusPath, {
entryPoints: originPath + "src",
out: targetPath,
entryDocument: "index.md",
hideInPageTOC: true,
hideBreadcrumbs: true,
watch: false,
tsconfig: originPath + "tsconfig.json",
sidebar: {
sidebarFile,
},
readme: "none",
});
console.log("Tasks completed!");
} catch (error) {
throw error
throw error;
// core.setFailed(error.message);
}
})();
})();

13
main.js
View File

@@ -1,13 +0,0 @@
const transformDocs = async (folderPath, originPath, targetPath) => {
await transform(contents, crateName);
await save(results, originPath, targetPath);
return results;
};
module.exports = {
transformDocs,
};

View File

@@ -4,7 +4,9 @@
"main": "index.js",
"license": "MIT",
"dependencies": {
"@types/node": "^14.14.35",
"typedoc": "^0.20.30",
"typedoc-plugin-markdown": "^3.6.0"
"typedoc-plugin-markdown": "^3.6.0",
"typescript": "^4.2.3"
}
}

126
src/front-matter.ts Normal file
View File

@@ -0,0 +1,126 @@
import * as path from 'path';
import { BindOption } from 'typedoc';
import { Component } from 'typedoc/dist/lib/converter/components';
import { RendererComponent } from 'typedoc/dist/lib/output/components';
import { PageEvent } from 'typedoc/dist/lib/output/events';
import { FrontMatter, Sidebar } from './types';
import { reflectionTitle } from 'typedoc-plugin-markdown/dist/resources/helpers/reflection-title';
export interface FrontMatterVars {
[key: string]: string | number | boolean;
}
/**
* Prepends YAML block to a string
* @param contents - the string to prepend
* @param vars - object of required front matter variables
*/
export const prependYAML = (contents: string, vars: FrontMatterVars) => {
return contents
.replace(/^/, toYAML(vars) + '\n\n')
.replace(/[\r\n]{3,}/g, '\n\n');
};
/**
* Returns the page title as rendered in the document h1(# title)
* @param page
*/
export const getPageTitle = (page: PageEvent) => {
return reflectionTitle.call(page, false);
};
/**
* Converts YAML object to a YAML string
* @param vars
*/
const toYAML = (vars: FrontMatterVars) => {
const yaml = `---
${Object.entries(vars)
.map(
([key, value]) =>
`${key}: ${
typeof value === 'string' ? `"${escapeString(value)}"` : value
}`,
)
.join('\n')}
---`;
return yaml;
};
// prettier-ignore
const escapeString=(str: string) => str.replace(/([^\\])'/g, '$1\\\'');
@Component({ name: 'front-matter' })
export class FrontMatterComponent extends RendererComponent {
@BindOption('out')
out!: string;
@BindOption('sidebar')
sidebar!: Sidebar;
@BindOption('globalsTitle')
globalsTitle!: string;
@BindOption('readmeTitle')
readmeTitle!: string;
@BindOption('entryDocument')
entryDocument!: string;
globalsFile = 'modules.md';
initialize() {
super.initialize();
this.listenTo(this.application.renderer, {
[PageEvent.END]: this.onPageEnd,
});
}
onPageEnd(page: PageEvent) {
if (page.contents) {
page.contents = prependYAML(page.contents, this.getYamlItems(page));
}
}
getYamlItems(page: PageEvent): any {
const pageTitle = this.getTitle(page);
const sidebarLabel = this.getSidebarLabel(page);
let items: FrontMatter = {
title: pageTitle,
};
if (sidebarLabel && sidebarLabel !== pageTitle) {
items = { ...items, sidebar_label: sidebarLabel };
}
return {
...items,
custom_edit_url: null,
hide_title: true,
};
}
getSidebarLabel(page: PageEvent) {
if (!this.sidebar) {
return null;
}
if (page.url === this.entryDocument) {
return page.url === page.project.url
? this.sidebar.indexLabel
: this.sidebar.readmeLabel;
}
if (page.url === this.globalsFile) {
return this.sidebar.indexLabel;
}
return this.sidebar.fullNames ? page.model.getFullName() : page.model.name;
}
getId(page: PageEvent) {
return path.basename(page.url, path.extname(page.url));
}
getTitle(page: PageEvent) {
const readmeTitle = this.readmeTitle || page.project.name;
if (page.url === this.entryDocument && page.url !== page.project.url) {
return readmeTitle;
}
return getPageTitle(page);
}
}

1
src/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default } from './plugin';

113
src/options.ts Normal file
View File

@@ -0,0 +1,113 @@
import * as path from 'path';
import {
Application,
MixedDeclarationOption,
ParameterType,
StringDeclarationOption,
TSConfigReader,
TypeDocReader,
} from 'typedoc';
import { PluginOptions, SidebarOptions } from './types';
/**
* Default plugin options
*/
const DEFAULT_PLUGIN_OPTIONS: PluginOptions = {
id: 'default',
docsRoot: 'docs',
out: 'api',
entryDocument: 'index.md',
hideInPageTOC: true,
hideBreadcrumbs: true,
sidebar: {
fullNames: false,
sidebarFile: 'typedoc-sidebar.js',
indexLabel: 'Table of contents',
readmeLabel: 'Readme',
sidebarPath: '',
},
plugin: ['none'],
outputDirectory: '',
siteDir: '',
watch: false,
};
/**
* Merge default with user options
* @param opts
*/
export const getOptions = (
siteDir: string,
opts: Partial<PluginOptions>,
): PluginOptions => {
// base options
let options = {
...DEFAULT_PLUGIN_OPTIONS,
...opts,
};
// sidebar
if (opts.sidebar === null) {
options = { ...options, sidebar: null };
} else {
const sidebar = {
...DEFAULT_PLUGIN_OPTIONS.sidebar,
...opts.sidebar,
} as SidebarOptions;
options = {
...options,
sidebar: {
...sidebar,
sidebarPath: path.resolve(siteDir, sidebar.sidebarFile),
},
};
}
// additional
options = {
...options,
siteDir,
outputDirectory: path.resolve(siteDir, options.docsRoot, options.out),
};
return options;
};
/**
* Add docusaurus options to converter
* @param app
*/
export const addOptions = (app: Application) => {
// configure deault typedoc options
app.options.addReader(new TypeDocReader());
app.options.addReader(new TSConfigReader());
// expose plugin options to typedoc so we can access if required
app.options.addDeclaration({
name: 'id',
} as StringDeclarationOption);
app.options.addDeclaration({
name: 'docsRoot',
} as StringDeclarationOption);
app.options.addDeclaration({
name: 'siteDir',
} as MixedDeclarationOption);
app.options.addDeclaration({
name: 'outputDirectory',
} as StringDeclarationOption);
app.options.addDeclaration({
name: 'globalsTitle',
} as StringDeclarationOption);
app.options.addDeclaration({
name: 'readmeTitle',
} as StringDeclarationOption);
app.options.addDeclaration({
name: 'sidebar',
type: ParameterType.Mixed,
} as MixedDeclarationOption);
};

52
src/plugin.ts Normal file
View File

@@ -0,0 +1,52 @@
import { Application } from 'typedoc';
import * as MarkdownPlugin from 'typedoc-plugin-markdown';
import { FrontMatterComponent } from './front-matter';
import { addOptions, getOptions } from './options';
import { render } from './render';
import { SidebarComponent } from './sidebar';
import { PluginOptions } from './types';
export default async function generate(
siteDir: string,
opts: Partial<PluginOptions>,
) {
// we need to generate an empty sidebar up-front so it can be resolved from sidebars.js
const options = getOptions(siteDir, opts);
// if (options.sidebar) {
// writeSidebar(options.sidebar, 'module.exports=[];');
// }
// initialize and build app
const app = new Application();
// load the markdown plugin
MarkdownPlugin(app);
// customise render
app.renderer.render = render;
// add plugin options
addOptions(app);
// bootstrap typedoc app
app.bootstrap(options);
// add frontmatter component to typedoc renderer
app.renderer.addComponent('fm', new FrontMatterComponent(app.renderer));
// add sidebar component to typedoc renderer
app.renderer.addComponent('sidebar', new SidebarComponent(app.renderer));
// return the generated reflections
const project = app.convert();
// if project is undefined typedoc has a problem - error logging will be supplied by typedoc.
if (!project) {
return;
}
// generate or watch app
return app.generateDocs(project, options.outputDirectory);
}

34
src/render.ts Normal file
View File

@@ -0,0 +1,34 @@
import { ProjectReflection, UrlMapping } from 'typedoc';
import { RendererEvent } from 'typedoc/dist/lib/output/events';
import * as ts from 'typescript';
export async function render(
project: ProjectReflection,
outputDirectory: string,
) {
if (!this.prepareTheme() || !this.prepareOutputDirectory(outputDirectory)) {
return;
}
const output = new RendererEvent(
RendererEvent.BEGIN,
outputDirectory,
project,
);
output.settings = this.application.options.getRawValues();
output.urls = this.theme!.getUrls(project);
this.trigger(output);
if (!output.isDefaultPrevented) {
output.urls?.forEach((mapping: UrlMapping, i) => {
this.renderDocument(output.createPageEvent(mapping));
ts.sys.write(
`\rGenerated ${i + 1} of ${output.urls?.length} TypeDoc docs`,
);
});
ts.sys.write(`\n`);
this.trigger(RendererEvent.END, output);
}
}

112
src/sidebar.ts Normal file
View File

@@ -0,0 +1,112 @@
import * as fs from 'fs';
import * as path from 'path';
import { BindOption } from 'typedoc';
import { Component } from 'typedoc/dist/lib/converter/components';
import { RendererComponent } from 'typedoc/dist/lib/output/components';
import { RendererEvent } from 'typedoc/dist/lib/output/events';
import { SidebarItem, SidebarOptions } from './types';
import { readFile, writeFile } from 'fs/promises';
@Component({ name: 'sidebar' })
export class SidebarComponent extends RendererComponent {
@BindOption('sidebar')
sidebar!: SidebarOptions;
@BindOption('siteDir')
siteDir!: string;
@BindOption('out')
out!: string;
initialize() {
this.listenTo(this.application.renderer, {
[RendererEvent.BEGIN]: this.onRendererBegin,
});
}
async onRendererBegin(renderer: RendererEvent) {
const navigation = this.application.renderer.theme?.getNavigation(
renderer.project,
);
const out = this.out.match(/(?:.*)en\/(.*)/)![1];
// map the navigation object to a Docuaurus sidebar format
const sidebarItems = navigation?.children
? navigation.children.map((navigationItem) => {
if (navigationItem.isLabel) {
const sidebarCategoryItems = navigationItem.children
? navigationItem.children.map((navItem) => {
const url = this.getUrlKey(out, navItem.url);
if (navItem.children && navItem.children.length > 0) {
const sidebarCategoryChildren = navItem.children.map(
(childGroup) =>
this.getSidebarCategory(
childGroup.title,
childGroup.children
? childGroup.children.map((childItem) =>
this.getUrlKey(out, childItem.url),
)
: [],
),
);
return this.getSidebarCategory(navItem.title, [
url,
...sidebarCategoryChildren,
]);
}
return url;
})
: [];
return this.getSidebarCategory(
navigationItem.title,
sidebarCategoryItems,
);
}
return this.getUrlKey(out, navigationItem.url);
})
: [];
const sidebarPath = this.sidebar.sidebarPath;
const sidebarContent = JSON.parse(await readFile(sidebarPath!, "utf-8"));
const index = sidebarContent.docs[3].items
.map((row, index) => (row.label && row.label === "JavaScript" ? index : 0))
.reduce((accumulator, value) => accumulator + value);
sidebarContent.docs[3].items[index].items = sidebarItems; // Specify where to put the items
writeFile(sidebarPath, JSON.stringify(sidebarContent, null, 2));
this.application.logger.success(
`TypeDoc sidebar written to ${sidebarPath}`,
);
}
/**
* returns a sidebar category node
*/
getSidebarCategory(title: string, items: SidebarItem[]) {
return {
type: 'category',
label: title,
items,
};
}
/**
* returns the url key for relevant doc
*/
getUrlKey(out: string, url: string) {
const urlKey = url.replace('.md', '');
return out ? out + '/' + urlKey : urlKey;
}
}
/**
* Write content to sidebar file
*/
export const writeSidebar = (sidebar: SidebarOptions, content: string) => {
if (!fs.existsSync(path.dirname(sidebar.sidebarPath))) {
fs.mkdirSync(path.dirname(sidebar.sidebarPath));
}
fs.writeFileSync(sidebar.sidebarPath, content);
};

46
src/types.ts Normal file
View File

@@ -0,0 +1,46 @@
export interface PluginOptions {
id: string;
docsRoot: string;
out: string;
sidebar: SidebarOptions | null;
readmeTitle?: string;
globalsTitle?: string;
plugin: string[];
readme?: string;
disableOutputCheck?: boolean;
entryPoints?: string[];
entryDocument: string;
hideInPageTOC: boolean;
hideBreadcrumbs: boolean;
siteDir: string;
outputDirectory: string;
watch: boolean;
}
export interface FrontMatter {
id?: string;
title: string;
slug?: string;
sidebar_label?: string;
hide_title?: boolean;
}
export interface SidebarOptions {
fullNames?: boolean;
sidebarFile: string;
sidebarPath: string;
indexLabel?: string;
readmeLabel?: string;
}
export interface Sidebar {
[sidebarId: string]: SidebarItem[];
}
export interface SidebarCategory {
type: string;
label: string;
items: SidebarItem[];
}
export type SidebarItem = SidebarCategory | string;

23
src/watch.ts Normal file
View File

@@ -0,0 +1,23 @@
import * as fs from 'fs';
import * as path from 'path';
import { Application } from 'typedoc';
import { PluginOptions } from './types';
/**
* Calls TypeDoc's `convertAndWatch` and force trigger sidebars refresh.
*/
export const convertAndWatch = (app: Application, options: PluginOptions) => {
const sidebarsJsPath = path.resolve(options.siteDir, 'sidebars.js');
app.convertAndWatch(async (project) => {
if (options.sidebar) {
// remove typedoc sidebar from require cache
delete require.cache[options.sidebar.sidebarPath];
// force trigger a sidebars.js refresh
const sidebarJsContent = fs.readFileSync(sidebarsJsPath);
fs.writeFileSync(sidebarsJsPath, sidebarJsContent);
}
await app.generateDocs(project, options.outputDirectory);
});
};

15
tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"declaration": true,
"experimentalDecorators": true,
"lib": ["es2018", "dom"],
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": false,
"noUnusedLocals": true,
"removeComments": false,
"sourceMap": false,
"strictNullChecks": true,
"target": "es2018"
}
}

View File

@@ -2,6 +2,11 @@
# yarn lockfile v1
"@types/node@^14.14.35":
version "14.14.35"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.35.tgz#42c953a4e2b18ab931f72477e7012172f4ffa313"
integrity sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==
at-least-node@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
@@ -254,6 +259,11 @@ typedoc@^0.20.30:
shiki "^0.9.2"
typedoc-default-themes "^0.12.8"
typescript@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3"
integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==
uglify-js@^3.1.4:
version "3.13.0"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.0.tgz#66ed69f7241f33f13531d3d51d5bcebf00df7f69"