Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
- fix: handle BigInt values when instantiating the buffer in `lebEncode` and `slebEncode` from `@dfinity/candid`. As a result, `@dfinity/candid` now correctly encodes large bigints as `Nat` values.
- fix: make `.ts` extension required for all relative imports. This is required to avoid the "Module not found" error when importing the packages in Node.js (ESM).

## Added
### Added

- feat: Starlight documentation website, with custom plugin for typedoc

Expand Down
1 change: 1 addition & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
dist/
src/content/tmp/
src/content/docs/libs/
src/content/docs/changelog.md

# generated types
.astro/
Expand Down
19 changes: 17 additions & 2 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,28 @@ export default defineConfig({
plugins: [
libsPlugin({
baseDir: '../packages',
typeDoc: { exclude: ['../packages/core'] },
typeDoc: {
exclude: ['../packages/core'],
},
frontmatter: { editUrl: false, next: true, prev: true },
additionalFiles: [
{
path: '../CHANGELOG.md',
frontmatter: {
tableOfContents: { minHeadingLevel: 2, maxHeadingLevel: 2 },
pagefind: false,
editUrl: false,
next: false,
prev: false,
},
},
],
}),
],
sidebar: [
{
label: 'Release Notes',
autogenerate: { directory: 'release-notes' },
autogenerate: { directory: 'release-notes', collapsed: true },
},
],
}),
Expand Down
102 changes: 70 additions & 32 deletions docs/src/libs-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ import { Application, ProjectReflection, ReflectionKind, type TypeDocOptions } f

import { type AstroIntegrationLogger, type RemarkPlugins } from 'astro';
import type { StarlightPlugin } from '@astrojs/starlight/types';
import { docsSchema } from '@astrojs/starlight/schema';
import type { z } from 'astro/zod';
import { type PluginOptions as TypeDocMarkdownOptions } from 'typedoc-plugin-markdown';
import { visit } from 'unist-util-visit';
import yaml from 'yaml';

const CONTENT_DIR = path.resolve('src', 'content');
const TMP_DIR = path.resolve(CONTENT_DIR, 'tmp');
const DOCS_DIR = path.resolve(CONTENT_DIR, 'docs');

export type Frontmatter = Partial<z.infer<ReturnType<ReturnType<typeof docsSchema>>>>;

async function generateApiDocs({
baseDir,
typeDoc,
Expand Down Expand Up @@ -76,6 +81,16 @@ export interface LibsLoaderOptions {
* Options for TypeDoc.
*/
typeDoc?: LibsLoaderTypeDocOptions;

/**
* Additional files to include in the documentation.
*/
additionalFiles?: { path: string; frontmatter?: Frontmatter }[];

/**
* Frontmatter applied to every markdown file generated by TypeDoc.
*/
frontmatter?: Frontmatter;
}

export type LibsLoaderTypeDocOptions = TypeDocMarkdownOptions & TypeDocOptions;
Expand All @@ -100,8 +115,7 @@ const prettyUrlsPlugin: RemarkPlugin =
url.startsWith('https://') ||
url.startsWith('/') ||
url.startsWith('http://') ||
url.startsWith('mailto:') ||
url.startsWith('#')
url.startsWith('mailto:')
) {
logger.debug(`Skipping URL: ${url}`);
return;
Expand All @@ -110,7 +124,7 @@ const prettyUrlsPlugin: RemarkPlugin =
// normalize all other relative URLs to the docs directory
const absoluteLinkedFilePath = path.resolve(currentFileDir, url);
const relativeToDocs = path.relative(docsDir, absoluteLinkedFilePath);
const normalizedUrl = `/${relativeToDocs.replace(/(index)?\.mdx?$/, '').toLowerCase()}`;
const normalizedUrl = `/${relativeToDocs.replace(/(index)?\.mdx?(#.*)?$/, '$2').toLowerCase()}`;
logger.debug(`Normalizing URL: ${url} -> ${normalizedUrl}`);

node.url = normalizedUrl;
Expand Down Expand Up @@ -154,7 +168,7 @@ export function libsPlugin(opts: LibsLoaderOptions): StarlightPlugin {

const project = await generateApiDocs(opts);

const sidebarItems = [];
const librarySidebarItems = [];
const modules = project.getChildrenByKind(ReflectionKind.Module);
for (const { name } of modules) {
const id = name.startsWith('@') ? name.split('/')[1] : name;
Expand All @@ -165,7 +179,10 @@ export function libsPlugin(opts: LibsLoaderOptions): StarlightPlugin {
await processMarkdown({
inputPath: path.resolve(baseDir, id, 'README.md'),
outputPath: path.resolve(outputRootDir, `index.md`),
title,
frontmatter: {
title,
...(opts?.frontmatter || {}),
},
});

const apiSrcDir = path.resolve(TMP_DIR, name);
Expand All @@ -179,17 +196,20 @@ export function libsPlugin(opts: LibsLoaderOptions): StarlightPlugin {
const inputFileName = file.name;
const isReadme = inputFileName.endsWith('README.md');
const outputFileName = isReadme ? 'index.md' : inputFileName;
const title = isReadme ? 'Overview' : titleFromId(file.name.replace(/\.mdx?$/, ''));
const title = isReadme ? 'Overview' : titleFromFilename(file.name);

await processMarkdown({
inputPath: path.resolve(apiSrcDir, prefix, inputFileName),
outputPath: path.resolve(outputApiDir, prefix, outputFileName),
title,
frontmatter: {
title,
...(opts?.frontmatter || {}),
},
});
}
}

sidebarItems.push({
librarySidebarItems.push({
label: title,
collapsed: true,
items: [
Expand All @@ -209,8 +229,33 @@ export function libsPlugin(opts: LibsLoaderOptions): StarlightPlugin {
});
}

const sidebarItems = [];
for (const file of opts.additionalFiles || []) {
const fileName = path.basename(file.path).toLowerCase();
const id = idFromFilename(fileName);
const title = titleFromId(id);

await processMarkdown({
inputPath: path.resolve(file.path),
outputPath: path.resolve(DOCS_DIR, fileName),
frontmatter: {
title,
...(file.frontmatter || {}),
},
});

sidebarItems.push({
label: title,
link: `/${id}`,
});
}

ctx.updateConfig({
sidebar: [{ label: 'Libraries', items: sidebarItems }, ...(ctx.config.sidebar || [])],
sidebar: [
{ label: 'Libraries', items: librarySidebarItems },
...(ctx.config.sidebar || []),
...sidebarItems,
],
});
},
},
Expand All @@ -222,51 +267,44 @@ async function writeFile(filePath: string, content: string): Promise<void> {
await fs.writeFile(filePath, content, 'utf-8');
}

function idFromFilename(fileName: string): string {
return fileName.replace(/\.mdx?$/, '');
}

function titleFromFilename(fileName: string): string {
return titleFromId(idFromFilename(fileName));
}

function titleFromId(id: string): string {
return id.replace(/-/g, ' ').replace(/\b\w/g, char => char.toUpperCase());
}

interface ProcessMarkdownOpts {
inputPath: string;
outputPath: string;
title: string;
frontmatter: Frontmatter;
}

async function processMarkdown({
inputPath,
outputPath,
title,
frontmatter,
}: ProcessMarkdownOpts): Promise<void> {
const input = await fs.readFile(inputPath, 'utf-8');

const output = addFrontmatter(input, {
title,
editUrl: false,
next: true,
prev: true,
}).replaceAll('README.md', 'index.md');
const output = addFrontmatter(input, frontmatter)
.replace(/^\s*#\s*.*$/m, '')
.replaceAll('README.md', 'index.md');

await writeFile(outputPath, output);
}

function addFrontmatter(content: string, frontmatter: Record<string, boolean | string>) {
const frontmatterStr = Object.entries(frontmatter)
.map(([key, value]) => {
if (typeof value === 'boolean' || typeof value === 'number') {
return `${key}: ${value}`;
}

if (typeof value === 'string') {
return `${key}: "${value}"`;
}

throw new Error(`Invalid frontmatter value for key "${key}": ${value}`);
})
.join('\n');
function addFrontmatter(content: string, frontmatter: Frontmatter): string {
const frontmatterStr = yaml.stringify(frontmatter);

if (frontmatterStr.length === 0) {
return content;
}

return `---\n${frontmatterStr}\n---\n\n${content}`;
return `---\n${frontmatterStr}---\n\n${content}`;
}
Loading