Skip to content

Commit 7fae16a

Browse files
committed
Use typescript for plugins
1 parent 3dbc7a1 commit 7fae16a

12 files changed

+851
-297
lines changed

docusaurus.config.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import type * as Preset from '@docusaurus/preset-classic';
22
import type { Config } from '@docusaurus/types';
33

4-
import rehypeCodeblockMeta from './src/plugins/rehype-codeblock-meta.mjs';
5-
import rehypeStaticToDynamic from './src/plugins/rehype-static-to-dynamic.mjs';
6-
import rehypeVideoAspectRatio from './src/plugins/rehype-video-aspect-ratio.mjs';
7-
import remarkNpm2Yarn from './src/plugins/remark-npm2yarn.mjs';
8-
import remarkRawMarkdown from './src/plugins/remark-raw-markdown.mjs';
4+
import disableFullySpecified from './src/plugins/disable-fully-specified.ts';
5+
import llmsTxt from './src/plugins/llms-txt.ts';
6+
import ogImage from './src/plugins/og-image.ts';
7+
import reactNavigationVersions from './src/plugins/react-navigation-versions.ts';
8+
import rehypeCodeblockMeta from './src/plugins/rehype-codeblock-meta.ts';
9+
import rehypeStaticToDynamic from './src/plugins/rehype-static-to-dynamic.ts';
10+
import rehypeVideoAspectRatio from './src/plugins/rehype-video-aspect-ratio.ts';
11+
import remarkNpm2Yarn from './src/plugins/remark-npm2yarn.ts';
12+
import remarkRawMarkdown from './src/plugins/remark-raw-markdown.ts';
913
import darkTheme from './src/themes/react-navigation-dark';
1014
import lightTheme from './src/themes/react-navigation-light';
1115

@@ -131,10 +135,10 @@ const config: Config = {
131135
},
132136
} satisfies Preset.ThemeConfig,
133137
plugins: [
134-
'./src/plugins/disable-fully-specified.mjs',
135-
'./src/plugins/react-navigation-versions.mjs',
136-
['./src/plugins/llms-txt.mjs', { latestVersion }],
137-
'./src/plugins/og-image.ts',
138+
disableFullySpecified,
139+
reactNavigationVersions,
140+
[llmsTxt, { latestVersion }],
141+
ogImage,
138142
[
139143
'@docusaurus/plugin-client-redirects',
140144
{

__tests__/rehype-static-to-dynamic.test.mjs renamed to src/__tests__/rehype-static-to-dynamic.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import dedent from 'dedent';
22
import assert from 'node:assert';
33
import { describe, test } from 'node:test';
4-
import rehypeStaticToDynamic from '../src/plugins/rehype-static-to-dynamic.mjs';
4+
import rehypeStaticToDynamic from '../plugins/rehype-static-to-dynamic.ts';
55

66
/**
77
* Helper function to create a test tree structure

src/pages/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ export default function Home() {
1212

1313
return (
1414
<Layout
15-
// @ts-expect-error this are needed but don't exist in the type
1615
title={siteConfig.title}
1716
description={siteConfig.tagline}
1817
wrapperClassName="full-width"
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
export default function disableFullySpecified(context, options) {
1+
import type { LoadContext, Plugin } from '@docusaurus/types';
2+
3+
export default function disableFullySpecified(
4+
_context: LoadContext,
5+
_options: unknown
6+
): Plugin {
27
return {
38
name: 'disable-fully-specified',
49
configureWebpack() {
Lines changed: 127 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,103 @@
11
import fs from 'node:fs';
22
import path from 'node:path';
33
import util from 'node:util';
4-
import versions from '../../versions.json';
4+
import type { LoadContext, Plugin } from '@docusaurus/types';
5+
import versionsData from '../../versions.json';
6+
7+
type FrontMatterData = Record<string, string>;
8+
9+
type ParsedFrontMatter = {
10+
data: FrontMatterData;
11+
content: string;
12+
};
13+
14+
type SidebarCategory = {
15+
type: 'category';
16+
label: string;
17+
items: SidebarItem[];
18+
};
19+
20+
type SidebarItem = string | SidebarCategory | Record<string, unknown>;
21+
22+
type FullDoc = {
23+
title: string;
24+
url: string;
25+
content: string;
26+
};
27+
28+
type LlmsTxtOptions = {
29+
latestVersion?: string;
30+
};
31+
32+
type SidebarProcessResult = {
33+
content: string;
34+
docs: FullDoc[];
35+
};
36+
37+
const versionsSource: unknown = versionsData;
38+
39+
const versions = Array.isArray(versionsSource)
40+
? versionsSource.filter(
41+
(version): version is string => typeof version === 'string'
42+
)
43+
: [];
44+
45+
function isRecord(value: unknown): value is Record<string, unknown> {
46+
return typeof value === 'object' && value !== null && !Array.isArray(value);
47+
}
48+
49+
function hasOwnProperty<T extends object, K extends PropertyKey>(
50+
value: T,
51+
key: K
52+
): value is T & Record<K, unknown> {
53+
return Object.prototype.hasOwnProperty.call(value, key);
54+
}
55+
56+
function isSidebarItem(value: unknown): value is SidebarItem {
57+
return typeof value === 'string' || isRecord(value);
58+
}
59+
60+
function isSidebarCategory(item: SidebarItem): item is SidebarCategory {
61+
if (!isRecord(item)) {
62+
return false;
63+
}
64+
65+
if (
66+
!hasOwnProperty(item, 'type') ||
67+
!hasOwnProperty(item, 'label') ||
68+
!hasOwnProperty(item, 'items')
69+
) {
70+
return false;
71+
}
72+
73+
return (
74+
item.type === 'category' &&
75+
typeof item.label === 'string' &&
76+
Array.isArray(item.items)
77+
);
78+
}
79+
80+
function normalizeRootItems(value: unknown): SidebarItem[] {
81+
if (Array.isArray(value)) {
82+
return value.filter(isSidebarItem);
83+
}
84+
85+
if (isRecord(value)) {
86+
const normalized: SidebarCategory[] = [];
87+
88+
for (const [label, items] of Object.entries(value)) {
89+
normalized.push({
90+
type: 'category',
91+
label,
92+
items: Array.isArray(items) ? items.filter(isSidebarItem) : [],
93+
});
94+
}
95+
96+
return normalized;
97+
}
98+
99+
return [];
100+
}
5101

6102
/**
7103
* Parses frontmatter from markdown content.
@@ -10,7 +106,7 @@ import versions from '../../versions.json';
10106
* @param {string} fileContent - Raw markdown file content.
11107
* @returns {{data: Object, content: string}} - Parsed data and stripped content.
12108
*/
13-
function parseFrontMatter(fileContent) {
109+
function parseFrontMatter(fileContent: string): ParsedFrontMatter {
14110
const frontMatterRegex = /^---\n([\s\S]+?)\n---\n/;
15111
const match = fileContent.match(frontMatterRegex);
16112

@@ -21,7 +117,7 @@ function parseFrontMatter(fileContent) {
21117
const frontMatterBlock = match[1];
22118
const content = fileContent.replace(frontMatterRegex, '');
23119

24-
const data = {};
120+
const data: FrontMatterData = {};
25121

26122
frontMatterBlock.split('\n').forEach((line) => {
27123
const parts = line.split(':');
@@ -59,15 +155,15 @@ function parseFrontMatter(fileContent) {
59155
* @returns {{content: string, docs: Array}} - Generated index content and list of doc objects.
60156
*/
61157
function processSidebar(
62-
items,
63-
docsPath,
64-
version,
65-
isLatest,
66-
baseUrl,
158+
items: SidebarItem[],
159+
docsPath: string,
160+
version: string,
161+
isLatest: boolean,
162+
baseUrl: string,
67163
level = 0
68-
) {
164+
): SidebarProcessResult {
69165
let llmsContent = '';
70-
let fullDocsList = [];
166+
let fullDocsList: FullDoc[] = [];
71167

72168
items.forEach((item) => {
73169
// Case 1: Item is a direct link (string ID)
@@ -99,7 +195,7 @@ function processSidebar(
99195
}
100196
}
101197
// Case 2: Item is a category object
102-
else if (item.type === 'category') {
198+
else if (isSidebarCategory(item)) {
103199
const label = item.label;
104200
const headingPrefix = '#'.repeat(level + 3); // Start at level 3 (###)
105201

@@ -128,13 +224,13 @@ function processSidebar(
128224
* @returns {Array<string>} - List of generated filenames.
129225
*/
130226
function generateForVersion(
131-
siteDir,
132-
outDir,
133-
version,
134-
outputPrefix,
135-
isLatest,
136-
baseUrl
137-
) {
227+
siteDir: string,
228+
outDir: string,
229+
version: string,
230+
outputPrefix: string,
231+
isLatest: boolean,
232+
baseUrl: string
233+
): string[] {
138234
const docsPath = path.join(siteDir, 'versioned_docs', `version-${version}`);
139235
const sidebarPath = path.join(
140236
siteDir,
@@ -147,25 +243,18 @@ function generateForVersion(
147243
return [];
148244
}
149245

150-
const sidebarConfig = JSON.parse(fs.readFileSync(sidebarPath, 'utf8'));
246+
const sidebarConfig: unknown = JSON.parse(
247+
fs.readFileSync(sidebarPath, 'utf8')
248+
);
151249

152250
// Handle different Docusaurus sidebar structures (root 'docs' key or first key)
153-
let rootItems = sidebarConfig.docs || Object.values(sidebarConfig)[0] || [];
154-
155-
// Normalize object-style sidebars (older Docusaurus versions) to array format
156-
if (!Array.isArray(rootItems) && typeof rootItems === 'object') {
157-
const normalized = [];
158-
159-
for (const [label, items] of Object.entries(rootItems)) {
160-
normalized.push({
161-
type: 'category',
162-
label: label,
163-
items: items,
164-
});
165-
}
251+
const rootItemsSource = isRecord(sidebarConfig)
252+
? hasOwnProperty(sidebarConfig, 'docs')
253+
? sidebarConfig.docs
254+
: Object.values(sidebarConfig)[0]
255+
: undefined;
166256

167-
rootItems = normalized;
168-
}
257+
const rootItems = normalizeRootItems(rootItemsSource);
169258

170259
const { content: sidebarContent, docs } = processSidebar(
171260
rootItems,
@@ -213,7 +302,10 @@ function generateForVersion(
213302
return [summaryFilename, fullFilename];
214303
}
215304

216-
export default function (context, options) {
305+
export default function llmsTxtPlugin(
306+
context: LoadContext,
307+
options: LlmsTxtOptions
308+
): Plugin {
217309
return {
218310
name: 'llms.txt',
219311
async postBuild({ siteDir, outDir }) {
Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { mkdir, readFile, writeFile } from 'node:fs/promises';
22
import { dirname, join } from 'node:path';
3+
import type {
4+
LoadContext,
5+
Plugin,
6+
PluginContentLoadedActions,
7+
} from '@docusaurus/types';
38

49
const CACHE_DIR = join(
510
process.cwd(),
@@ -8,32 +13,91 @@ const CACHE_DIR = join(
813
'react-navigation-versions'
914
);
1015

11-
const query = async (name, tag) => {
16+
type NpmPackage = {
17+
version: string;
18+
peerDependencies?: Record<string, string>;
19+
};
20+
21+
type VersionQuery = {
22+
tag: string;
23+
packages: string[];
24+
};
25+
26+
function isRecord(value: unknown): value is Record<string, unknown> {
27+
return typeof value === 'object' && value !== null && !Array.isArray(value);
28+
}
29+
30+
function isStringRecord(value: unknown): value is Record<string, string> {
31+
if (!isRecord(value)) {
32+
return false;
33+
}
34+
35+
return Object.values(value).every((entry) => typeof entry === 'string');
36+
}
37+
38+
function isNpmPackage(value: unknown): value is NpmPackage {
39+
if (!isRecord(value)) {
40+
return false;
41+
}
42+
43+
if (typeof value.version !== 'string') {
44+
return false;
45+
}
46+
47+
if (
48+
'peerDependencies' in value &&
49+
value.peerDependencies !== undefined &&
50+
!isStringRecord(value.peerDependencies)
51+
) {
52+
return false;
53+
}
54+
55+
return true;
56+
}
57+
58+
const query = async (name: string, tag: string): Promise<NpmPackage> => {
1259
const cached = join(CACHE_DIR, `${name}-${tag}.json`);
1360

14-
let pkg;
61+
let pkg: NpmPackage;
1562

1663
try {
17-
pkg = await fetch(`https://registry.npmjs.org/${name}/${tag}`).then((res) =>
18-
res.json()
19-
);
64+
const response = await fetch(`https://registry.npmjs.org/${name}/${tag}`);
65+
const data: unknown = await response.json();
66+
67+
if (!isNpmPackage(data)) {
68+
throw new Error(`Invalid package response for ${name}@${tag}`);
69+
}
70+
71+
pkg = data;
2072

2173
await mkdir(dirname(cached), { recursive: true });
2274
await writeFile(cached, JSON.stringify(pkg));
2375
} catch (e) {
2476
const data = await readFile(cached, 'utf-8');
77+
const parsed: unknown = JSON.parse(data);
78+
79+
if (!isNpmPackage(parsed)) {
80+
throw new Error(`Invalid cached package response for ${name}@${tag}`);
81+
}
2582

26-
pkg = JSON.parse(data);
83+
pkg = parsed;
2784
}
2885

2986
return pkg;
3087
};
3188

32-
export default function reactNavigationVersionsPlugin(context, options) {
89+
export default function reactNavigationVersionsPlugin(
90+
_context: LoadContext,
91+
_options: unknown
92+
): Plugin {
3393
return {
3494
name: 'react-navigation-versions',
35-
async contentLoaded({ content, actions }) {
36-
const queries = {
95+
async contentLoaded({
96+
actions,
97+
}: {
98+
actions: PluginContentLoadedActions;
99+
}) {
100+
const queries: Record<string, VersionQuery> = {
37101
'7.x': {
38102
tag: 'latest',
39103
packages: [

0 commit comments

Comments
 (0)