Skip to content

Commit b8d8644

Browse files
authored
docs: convert the documentation site from GitBook to VitePress (#4349)
1 parent 775c1e0 commit b8d8644

13 files changed

Lines changed: 1507 additions & 30 deletions

File tree

.gitbook.yaml

Lines changed: 0 additions & 10 deletions
This file was deleted.

.github/workflows/docs.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Build and deploy the VitePress documentation site to GitHub Pages.
2+
name: Docs
3+
4+
on:
5+
push:
6+
branches: [master]
7+
paths:
8+
- 'docs/**'
9+
- '.github/workflows/docs.yml'
10+
- 'package.json'
11+
- 'yarn.lock'
12+
workflow_dispatch:
13+
14+
# Allow only one concurrent deployment, skipping runs queued between the run
15+
# in-progress and latest queued. Do not cancel in-progress runs so that a
16+
# deployment can complete.
17+
concurrency:
18+
group: pages
19+
cancel-in-progress: false
20+
21+
jobs:
22+
build:
23+
runs-on: ubuntu-latest
24+
permissions:
25+
contents: read
26+
steps:
27+
- name: Checkout
28+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
29+
with:
30+
fetch-depth: 0 # Needed for VitePress lastUpdated timestamps
31+
- name: Use Node.js
32+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
33+
with:
34+
node-version: lts/*
35+
cache: 'yarn'
36+
- name: Install dependencies
37+
run: yarn install --frozen-lockfile
38+
- name: Build with VitePress
39+
run: yarn docs:build
40+
- name: Upload artifact
41+
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
42+
with:
43+
path: docs/.vitepress/dist
44+
45+
deploy:
46+
needs: build
47+
runs-on: ubuntu-latest
48+
environment:
49+
name: github-pages
50+
url: ${{ steps.deployment.outputs.page_url }}
51+
permissions:
52+
pages: write # to deploy to Pages
53+
id-token: write # to verify the deployment originates from an appropriate source
54+
steps:
55+
- name: Setup Pages
56+
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
57+
- name: Deploy to GitHub Pages
58+
id: deployment
59+
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ yarn-error.log*
1414
coverage
1515
coverage/*
1616

17+
# VitePress build artifacts
18+
docs/.vitepress/dist
19+
docs/.vitepress/cache
20+
1721
# Editor directories and files
1822
.idea
1923
.vscode

docs/.vitepress/config.mts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { defineConfig } from 'vitepress';
2+
import { markdownItGitbook } from './gitbook';
3+
import { loadNavigation } from './sidebar';
4+
5+
const { nav, sidebar } = loadNavigation();
6+
7+
// https://vitepress.dev/reference/site-config
8+
export default defineConfig({
9+
title: 'BullMQ',
10+
description:
11+
'The fastest, most reliable, Redis-based distributed queue for Node. ' +
12+
'Carefully written for rock solid stability and atomicity.',
13+
lang: 'en-US',
14+
cleanUrls: true,
15+
lastUpdated: true,
16+
17+
// Content lives in ./gitbook (migrated from the GitBook site).
18+
srcDir: 'gitbook',
19+
srcExclude: ['**/SUMMARY.md'],
20+
vite: {
21+
publicDir: 'gitbook/public',
22+
},
23+
24+
// GitBook used README.md as the index of each folder; map those to clean
25+
// directory URLs (e.g. guide/queues/README.md -> /guide/queues/).
26+
rewrites: (id: string) => id.replace(/(^|\/)README\.md$/, '$1index.md'),
27+
28+
// The migrated content contains many cross-links and anchors that are not
29+
// worth auditing as part of the migration; skip dead-link checking.
30+
ignoreDeadLinks: true,
31+
32+
markdown: {
33+
config: md => {
34+
md.use(markdownItGitbook);
35+
},
36+
},
37+
38+
themeConfig: {
39+
// https://vitepress.dev/reference/default-theme-config
40+
nav,
41+
sidebar,
42+
43+
search: {
44+
provider: 'local',
45+
},
46+
47+
socialLinks: [
48+
{ icon: 'github', link: 'https://github.com/taskforcesh/bullmq' },
49+
{ icon: 'discord', link: 'https://discord.gg/f4uq7dv' },
50+
],
51+
52+
editLink: {
53+
pattern:
54+
'https://github.com/taskforcesh/bullmq/edit/master/docs/gitbook/:path',
55+
text: 'Edit this page on GitHub',
56+
},
57+
58+
footer: {
59+
message: 'Released under the MIT License.',
60+
copyright: 'Copyright © 2018-present Taskforce.sh Inc.',
61+
},
62+
},
63+
});

docs/.vitepress/gitbook.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type MarkdownIt from 'markdown-it';
2+
3+
// Maps GitBook hint styles to VitePress custom-container types.
4+
const HINT_STYLES: Record<string, string> = {
5+
info: 'info',
6+
warning: 'warning',
7+
danger: 'danger',
8+
success: 'tip',
9+
};
10+
11+
/**
12+
* Converts GitBook-flavoured Markdown into the equivalent VitePress Markdown.
13+
*
14+
* The docs were originally authored for GitBook, which uses a handful of
15+
* `{% ... %}` block tags that VitePress does not understand. This rewrites the
16+
* most common ones so the existing content renders correctly:
17+
*
18+
* - `{% hint style="..." %}` -\> `::: info | tip | warning | danger`
19+
* - `{% tabs %}` / `{% tab %}` -\> `::: code-group` with labeled code fences
20+
* - `{% code title="..." %}` -\> a labeled `::: code-group` block
21+
*/
22+
export function transformGitbookMarkdown(src: string): string {
23+
let out = src;
24+
25+
// {% code title="file.ts" lineNumbers="true" %} ```lang ... ``` {% endcode %}
26+
out = out.replace(
27+
/\{%\s*code([^%]*)%\}\s*\r?\n```(\w*)([^\n]*)\n([\s\S]*?)```\s*\r?\n\{%\s*endcode\s*%\}/g,
28+
(_match, attrs: string, lang: string, fenceAttrs: string, code: string) => {
29+
const title = /title="([^"]*)"/.exec(attrs)?.[1];
30+
const lineNumbers = /lineNumbers="true"/.test(attrs)
31+
? ':line-numbers'
32+
: '';
33+
const label = title ? ` [${title}]` : '';
34+
return `::: code-group\n\`\`\`${lang}${lineNumbers}${label}${fenceAttrs}\n${code}\`\`\`\n:::`;
35+
},
36+
);
37+
38+
// {% tabs %} ... {% endtabs %} wrapping labelled {% tab title="..." %} blocks.
39+
out = out.replace(/\{%\s*tabs\s*%\}/g, '\n::: code-group\n');
40+
out = out.replace(/\{%\s*endtabs\s*%\}/g, '\n:::\n');
41+
// {% tab title="TypeScript" %} immediately followed by a code fence becomes a
42+
// labelled fence understood by the surrounding ::: code-group block.
43+
out = out.replace(
44+
/\{%\s*tab\s+title="([^"]*)"\s*%\}\s*\r?\n+```(\w*)/g,
45+
(_match, title: string, lang: string) => `\`\`\`${lang} [${title}]`,
46+
);
47+
// Drop any leftover tab markers (e.g. tabs that did not wrap a code fence).
48+
out = out.replace(/\{%\s*tab\s+title="[^"]*"\s*%\}/g, '');
49+
out = out.replace(/\{%\s*endtab\s*%\}/g, '');
50+
51+
// {% hint style="info" %} ... {% endhint %}
52+
out = out.replace(
53+
/\{%\s*hint\s+style="([^"]*)"\s*%\}/g,
54+
(_match, style: string) => {
55+
const type = HINT_STYLES[style] ?? 'info';
56+
return `\n::: ${type}\n`;
57+
},
58+
);
59+
out = out.replace(/\{%\s*endhint\s*%\}/g, '\n:::\n');
60+
61+
return out;
62+
}
63+
64+
/**
65+
* markdown-it plugin that rewrites GitBook syntax before block tokenization.
66+
*/
67+
export function markdownItGitbook(md: MarkdownIt): void {
68+
md.core.ruler.before('block', 'gitbook', state => {
69+
state.src = transformGitbookMarkdown(state.src);
70+
});
71+
}

docs/.vitepress/sidebar.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { readFileSync } from 'node:fs';
2+
import { fileURLToPath } from 'node:url';
3+
import { dirname, resolve } from 'node:path';
4+
import type { DefaultTheme } from 'vitepress';
5+
6+
const __dirname = dirname(fileURLToPath(import.meta.url));
7+
const SUMMARY_PATH = resolve(__dirname, '../gitbook/SUMMARY.md');
8+
9+
interface RawItem {
10+
text: string;
11+
target: string;
12+
depth: number;
13+
children: RawItem[];
14+
}
15+
16+
/**
17+
* Converts a GitBook SUMMARY link target into a VitePress link.
18+
*
19+
* External URLs are returned untouched. Local `.md` targets are turned into
20+
* clean, extension-less absolute links. `README.md` files map to their
21+
* containing directory to match the `rewrites` configured in `config.mts`.
22+
*/
23+
function toLink(target: string): string {
24+
const cleaned = target.replace(/^<(.*)>$/, '$1').trim();
25+
if (/^https?:\/\//.test(cleaned)) {
26+
return cleaned;
27+
}
28+
29+
let link = cleaned.replace(/\.md$/i, '');
30+
link = link.replace(/(^|\/)README$/i, '$1');
31+
if (!link.startsWith('/')) {
32+
link = `/${link}`;
33+
}
34+
return link;
35+
}
36+
37+
const LINK_RE = /^(\s*)-\s*\[([^\]]+)\]\(([^)]+)\)\s*$/;
38+
39+
/**
40+
* Parses the GitBook `SUMMARY.md` table of contents into VitePress `nav` and
41+
* `sidebar` structures, so the documentation navigation stays in sync with a
42+
* single source of truth.
43+
*/
44+
export function loadNavigation(): {
45+
nav: DefaultTheme.NavItem[];
46+
sidebar: DefaultTheme.SidebarItem[];
47+
} {
48+
const content = readFileSync(SUMMARY_PATH, 'utf-8');
49+
const lines = content.split('\n');
50+
51+
const sections: { title: string; items: RawItem[] }[] = [];
52+
let current: { title: string; items: RawItem[] } = {
53+
title: 'Overview',
54+
items: [],
55+
};
56+
sections.push(current);
57+
58+
for (const line of lines) {
59+
const sectionMatch = /^##\s+(.+?)\s*$/.exec(line);
60+
if (sectionMatch) {
61+
current = { title: sectionMatch[1], items: [] };
62+
sections.push(current);
63+
continue;
64+
}
65+
66+
const linkMatch = LINK_RE.exec(line);
67+
if (!linkMatch) {
68+
continue;
69+
}
70+
71+
const [, indent, text, target] = linkMatch;
72+
const depth = Math.floor(indent.length / 2);
73+
const item: RawItem = { text, target, depth, children: [] };
74+
75+
// Attach to the last item at depth - 1, otherwise it is a top-level item.
76+
let parent: RawItem | undefined;
77+
const stack = current.items;
78+
if (depth > 0) {
79+
let candidates: RawItem[] = stack;
80+
for (let level = 0; level < depth - 1; level++) {
81+
// Descend into the most recently added item's children. If it has none
82+
// yet (malformed indentation), keep the current level as a fallback.
83+
candidates = candidates[candidates.length - 1]?.children ?? candidates;
84+
}
85+
parent = candidates[candidates.length - 1];
86+
}
87+
88+
if (parent) {
89+
parent.children.push(item);
90+
} else {
91+
current.items.push(item);
92+
}
93+
}
94+
95+
const toSidebarItem = (item: RawItem): DefaultTheme.SidebarItem => {
96+
const node: DefaultTheme.SidebarItem = { text: item.text };
97+
node.link = toLink(item.target);
98+
if (item.children.length > 0) {
99+
node.collapsed = true;
100+
node.items = item.children.map(toSidebarItem);
101+
}
102+
return node;
103+
};
104+
105+
const sidebar: DefaultTheme.SidebarItem[] = sections
106+
.filter(section => section.items.length > 0)
107+
.map(section => ({
108+
text: section.title,
109+
collapsed: section.title !== 'Overview' && section.title !== 'Guide',
110+
items: section.items.map(toSidebarItem),
111+
}));
112+
113+
// Build a compact top navigation from the main sections.
114+
const sectionLink = (title: string): string | undefined => {
115+
const section = sections.find(s => s.title === title);
116+
const first = section?.items.find(i => !/^https?:\/\//.test(i.target));
117+
return first ? toLink(first.target) : undefined;
118+
};
119+
120+
const nav: DefaultTheme.NavItem[] = [];
121+
const guideLink = sectionLink('Guide');
122+
if (guideLink) {
123+
nav.push({ text: 'Guide', link: guideLink });
124+
}
125+
const patternsLink = sectionLink('Patterns');
126+
if (patternsLink) {
127+
nav.push({ text: 'Patterns', link: patternsLink });
128+
}
129+
130+
const languageDropdown: DefaultTheme.NavItemWithChildren = {
131+
text: 'Bindings',
132+
items: [],
133+
};
134+
for (const title of ['Python', 'Rust', 'Elixir', 'PHP']) {
135+
const link = sectionLink(title);
136+
if (link) {
137+
languageDropdown.items.push({ text: title, link });
138+
}
139+
}
140+
if (languageDropdown.items.length > 0) {
141+
nav.push(languageDropdown);
142+
}
143+
144+
const proLink = sectionLink('BullMQ Pro');
145+
if (proLink) {
146+
nav.push({ text: 'Pro', link: proLink });
147+
}
148+
149+
nav.push({ text: 'API Reference', link: 'https://api.docs.bullmq.io' });
150+
151+
return { nav, sidebar };
152+
}

docs/gitbook/SUMMARY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Table of contents
22

33
- [What is BullMQ](README.md)
4-
- [Quick Start](<README (1).md>)
4+
- [Quick Start](quick-start.md)
55
- [API Reference](https://api.docs.bullmq.io)
66
- [Changelogs](changelog.md)
77
- [v4](changelogs/changelog-v4.md)

docs/gitbook/guide/flows/remove-child-dependency.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const originalTree = await flow.add({
2424
await originalTree.children[0].job.removeChildDependency();
2525
```
2626

27-
{% hint style="waring" %}
27+
{% hint style="warning" %}
2828
As soon as a **child** calls this method, it will verify if it has an existing parent, if not, it'll throw an error.
2929
{% endhint %}
3030

0 commit comments

Comments
 (0)