Skip to content

Commit

Permalink
docs: add mermaid graphs
Browse files Browse the repository at this point in the history
  • Loading branch information
stijnvanhulle committed Sep 29, 2024
1 parent ebbfac2 commit 1dcfa40
Show file tree
Hide file tree
Showing 8 changed files with 1,382 additions and 44 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ test.json
.next
docs/.vitepress/cache/**
docs/.vitepress/dist/**
docs/.vitepress/graphs/**
!__snapshots__
kubb-files.json
kubb.log
Expand Down
6 changes: 6 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { groupIconMdPlugin, groupIconVitePlugin, localIconLoader } from 'vitepre
import { transformerTwoslash } from '@shikijs/vitepress-twoslash'
import { SitemapStream } from 'sitemap'
import { defineConfig } from 'vitepress'
import { withMermaid } from "vitepress-plugin-mermaid";

import { renderMermaidGraphsPlugin } from './mermaid';
import { transposeTables } from "./transposeTables.ts"
import { transposeTables } from './transposeTables.ts'

import { version } from '../../packages/core/package.json'
Expand Down Expand Up @@ -36,6 +40,7 @@ const knowledgeBaseSidebar = [
{
text: 'Plugins',
collapsed: false,
link: '/knowledge-base/plugins/',
items: [
{
text: 'Plugin system',
Expand Down Expand Up @@ -658,6 +663,7 @@ export default defineConfig({
},
vite: {
plugins: [
renderMermaidGraphsPlugin(),
groupIconVitePlugin({
customIcon: {
'kubb.config.ts': localIconLoader(import.meta.url, '../public/logo.svg'),
Expand Down
107 changes: 107 additions & 0 deletions docs/.vitepress/mermaid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { createHash } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import { readdir, mkdir, readFile, writeFile } from 'node:fs/promises';
import { run } from "@mermaid-js/mermaid-cli"

import type { Plugin } from 'vite';

async function getFilesInDirectory(directory: URL): Promise<string[]> {
return (await readdir(directory)).filter(file => file[0] !== '.');
}

const graphsDirectory = new URL('graphs/', import.meta.url);

const mermaidRegExp = /^```mermaid\n([\S\s]*?)\n```/gm;
const greaterThanRegExp = /&gt;/g;
const svgIdRegExp = /my-svg/g;
const styleTagRegExp = /<style>[\S\s]*?<\/style>/gm;

export function renderMermaidGraphsPlugin(): Plugin {
const existingGraphFileNamesPromise = mkdir(graphsDirectory, { recursive: true })
.then(() => getFilesInDirectory(graphsDirectory))
.then(files => new Set(files.filter(name => name.endsWith('.svg'))));
const existingGraphsByName = new Map<string, Promise<string>>();

async function renderGraph(codeBlock: string, outFile: string, hash: string) {
const existingGraphFileNames = await existingGraphFileNamesPromise;
const outFileURL = new URL(outFile, graphsDirectory);
if (!existingGraphFileNames.has(outFile)) {
console.warn(
`Pre-rendered file ${outFile} not found, rendering...\nIf this throws on Vercel, you need to run "npm run build:docs" locally first and commit the updated svg files.`
);
const inFileURL = new URL(`${outFile}.mmd`, graphsDirectory);
await writeFile(inFileURL, codeBlock);

await run(
fileURLToPath(
inFileURL
), fileURLToPath(outFileURL),{
parseMMDOptions: {
mermaidConfig: {
theme: "dark",
fontSize: 13,
flowchart: {
padding: 4,
useMaxWidth: true,
},
"themeCSS": "* { line-height: 1.5; } span.edgeLabel { padding: 2px 5px; }"
}
},
puppeteerConfig: {
"args": ["--no-sandbox"]
}
}
)
}
const outFileContent = await readFile(outFileURL, 'utf8');
// Styles need to be placed top-level, so we extract them and then
// prepend them, separated with a line-break
const extractedStyles: string[] = [];
let hasReplacedId = false;
const replacementId = `mermaid-${hash}`;
const baseGraph = outFileContent
// We need to replace some HTML entities
.replace(greaterThanRegExp, '>')
// First, we replace the default id with a unique, hash-based one
.replace(svgIdRegExp, () => {
hasReplacedId = true;
return replacementId;
})
.replace(styleTagRegExp, styleTag => {
extractedStyles.push(styleTag);
return '';
});
if (!hasReplacedId) {
throw new Error('Could not find expected id "my-svg"');
}
console.log('Extracted styles from mermaid chart:', extractedStyles.length);
return `${extractedStyles.join('')}\n${baseGraph}`;
}

return {
enforce: 'pre',
name: 'render-mermaid-charts',
async transform(code, id) {
if (id.endsWith('.md')) {
const renderedGraphs: string[] = [];
const mermaidCodeBlocks: string[] = [];
let match: RegExpExecArray | null = null;
while ((match = mermaidRegExp.exec(code)) !== null) {
mermaidCodeBlocks.push(match[1]);
}
await Promise.all(
mermaidCodeBlocks.map(async (codeBlock, index) => {
const hash = createHash('sha256').update(codeBlock).digest('hex').slice(0, 8);
const outFile = `mermaid-${hash}.svg`;
if (!existingGraphsByName.has(outFile)) {
existingGraphsByName.set(outFile, renderGraph(codeBlock, outFile, hash));
}
renderedGraphs[index] = await existingGraphsByName.get(outFile)!;
})
);
return code.replace(mermaidRegExp, () => renderedGraphs.shift()!);
}
}
};
}
35 changes: 34 additions & 1 deletion docs/.vitepress/theme/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
--vp-c-brand-lighter: var(--vp-c-brand);
--vp-c-brand-lightest: var(--vp-c-brand);
--vp-c-brand-dark: #854811;
--vp-c-brand-darker: #854811;
--vp-c-brand-darker: var(--vp-c-brand-dark);
--vp-c-brand-dimm: rgba(100, 108, 255, 0.08);
--vp-c-brand-1: var(--vp-c-brand);
--vp-c-brand-2: #f3c796;
Expand Down Expand Up @@ -233,6 +233,39 @@
.dark #VPSidebarNav > .group:nth-child(-n + 9):not(:first-child) > .VPSidebarItem > .item > .text::before {
filter: invert(1);
}

svg[id^='mermaid-'] {
line-height: 1.5;
background-color: transparent !important;
}

svg[id^='mermaid-'] .edgeLabel {
padding: 0 !important;
}

svg[id^='mermaid-'] .cluster rect {
fill: rgba(185, 185, 185, 0.14) !important;
stroke: rgba(185, 185, 185, 0.54) !important;
}

.dark svg[id^='mermaid-'] .flowchart-link {
stroke: lightgrey !important;
}

.dark svg[id^='mermaid-'] .marker {
stroke: lightgrey !important;
fill: lightgrey !important;
}

.dark svg[id^='mermaid-'] .edgeLabel p {
background-color: #585858 !important;
}

.dark svg[id^='mermaid-'] span {
color: #ccc !important;
}


/*#VPSidebarNav > .group > .VPSidebarItem > .item > .text {*/
/* display: flex;*/
/* align-items: center;*/
Expand Down
103 changes: 103 additions & 0 deletions docs/knowledge-base/plugins/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,107 @@ title: Plugins
outline: deep
---

# Plugins

[[toc]]

## Pre plugin
```mermaid
flowchart LR
options --> generate
```

<style>
.legend-grid {
display: grid;
grid-template-columns: max-content max-content;
grid-column-gap: 1rem;
}

.legend-rect {
display: inline-block;
width: 1rem;
height: 1rem;
border-radius: 1rem;
border: 1px solid var(--vp-c-brand-dark);
vertical-align: middle;
margin-right: 0.5rem;
}
</style>

<div class="legend-grid">
<div style="grid-column: 1; grid-row: 1">
<span class="legend-rect" style="background: var(--red)"></span>hookFirst
</div>
<div style="grid-column: 1; grid-row: 2">
<span class="legend-rect" style="background: var(--vp-c-brand)"></span>hookForPlugin
</div>
<div style="grid-column: 1; grid-row: 3">
<span class="legend-rect" style="background: var(--green)"></span>hookParallel
</div>
<div style="grid-column: 2; grid-row: 1">
<span class="legend-rect" style="background: var(--yellow)"></span>hookSeq
</div>
</div>

```mermaid
---
config:
layout: elk
---
flowchart
classDef default fill:#e1e1e1, color:#000;
classDef hookFirst fill:#ff6565,stroke:#000;
classDef hookForPlugin fill:#f58517,stroke:#000;
classDef hookParallel fill:#5bff89,stroke:#000;
classDef hookSeq fill:#ffee55,stroke:#f00;
buildEnd("buildEnd"):::hookParallel
click buildEnd "#buildend" _parent
buildStart("buildStart"):::hookParallel
click buildStart "#buildstart" _parent
resolvePath("resolvePath"):::hookForPlugin
click resolvePath "#resolvePath" _parent
resolveName("resolveName"):::hookForPlugin
click resolveName "#resolveName" _parent
safeBuild
--> setup
--> read
--> clean
--> pre
buildStart
--> createBarrelFiles
--> processFiles
--> post
--> buildEnd
--> clear
subgraph plugin[ Plugin x ]
pre
.-> buildStart
name
options
pre
post
context
resolvePath
resolveName
buildEnd
buildStart
end
```

```mermaid
flowchart LR
orderFiles
--> processFile
.-> getSource
.-> write
.-> processFile
```
2 changes: 2 additions & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
},
"dependencies": {
"@shikijs/vitepress-twoslash": "^1.21.0",
"mermaid": "^11.2.1",
"sitemap": "^8.0.0",
"vitepress": "^1.3.4",
"vitepress-plugin-group-icons": "^1.2.4",
Expand All @@ -45,6 +46,7 @@
"@kubb/react": "workspace:*",
"@types/node": "^20.16.10",
"@types/react": "^18.3.10",
"@mermaid-js/mermaid-cli": "^11.2.0",
"cross-env": "^7.0.3",
"react": "^18.3.1",
"unplugin-kubb": "workspace:^",
Expand Down
44 changes: 1 addition & 43 deletions packages/core/src/PluginManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,7 @@ import type {

type RequiredPluginLifecycle = Required<PluginLifecycle>

/**
* Get the type of the first argument in a function.
* @example Arg0<(a: string, b: number) => void> -> string
*/
type Argument0<H extends keyof PluginLifecycle> = Parameters<RequiredPluginLifecycle[H]>[0]

type Strategy = 'hookFirst' | 'hookForPlugin' | 'hookParallel' | 'hookReduceArg0' | 'hookSeq'
type Strategy = 'hookFirst' | 'hookForPlugin' | 'hookParallel' | 'hookSeq'

type Executer<H extends PluginLifecycleHooks = PluginLifecycleHooks> = {
message: string
Expand Down Expand Up @@ -395,42 +389,6 @@ export class PluginManager {
return results.filter((result) => result.status === 'fulfilled').map((result) => (result as PromiseFulfilledResult<Awaited<TOuput>>).value)
}

/**
* Chain all plugins, `reduce` can be passed through to handle every returned value. The return value of the first plugin will be used as the first parameter for the plugin after that.
*/
hookReduceArg0<H extends PluginLifecycleHooks>({
hookName,
parameters,
reduce,
message,
}: {
hookName: H
parameters: PluginParameter<H>
reduce: (reduction: Argument0<H>, result: ReturnType<ParseResult<H>>, plugin: Plugin) => PossiblePromise<Argument0<H> | null>
message: string
}): Promise<Argument0<H>> {
const [argument0, ...rest] = parameters
const plugins = this.#getSortedPlugins(hookName)

let promise: Promise<Argument0<H>> = Promise.resolve(argument0)
for (const plugin of plugins) {
promise = promise
.then((arg0) => {
const value = this.#execute({
strategy: 'hookReduceArg0',
hookName,
parameters: [arg0, ...rest] as PluginParameter<H>,
plugin,
message,
})
return value
})
.then((result) => reduce.call(this.#core.context, argument0, result as ReturnType<ParseResult<H>>, plugin)) as Promise<Argument0<H>>
}

return promise
}

/**
* Chains plugins
*/
Expand Down
Loading

0 comments on commit 1dcfa40

Please sign in to comment.