Enable remarkNpm by default, replace remarkInstall with it.

This commit is contained in:
Fuma Nama
2025-06-30 12:28:36 +08:00
parent f8d17097b4
commit d0f8a153c7
8 changed files with 183 additions and 104 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'fumadocs-docgen': minor
'fumadocs-core': patch
'fumadocs-mdx': patch
'@fumadocs/mdx-remote': patch
---
Enable `remarkNpm` by default, replace `remarkInstall` with it.
+1
View File
@@ -101,6 +101,7 @@
"hast-util-to-jsx-runtime": "^2.3.6",
"image-size": "^2.0.2",
"negotiator": "^1.0.0",
"npm-to-yarn": "^3.0.1",
"react-remove-scroll": "^2.7.1",
"remark": "^15.0.0",
"remark-gfm": "^4.0.1",
+1
View File
@@ -10,3 +10,4 @@ export * from './remark-admonition';
export * from './rehype-toc';
export * from './remark-code-tab';
export * from './remark-steps';
export * from './remark-npm';
+127
View File
@@ -0,0 +1,127 @@
import type { Root } from 'mdast';
import type { Transformer } from 'unified';
import { visit } from 'unist-util-visit';
import convert from 'npm-to-yarn';
import type { MdxJsxAttribute, MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
interface PackageManager {
name: string;
/**
* Convert from npm to another package manager
*/
command: (command: string) => string;
}
export interface RemarkNpmOptions {
/**
* Persist Tab value (Fumadocs UI only)
*
* @defaultValue false
*/
persist?:
| {
id: string;
}
| false;
packageManagers?: PackageManager[];
}
const aliases = ['npm', 'package-install'];
/**
* It generates multiple tabs of codeblocks for different package managers from a npm command codeblock.
*/
export function remarkNpm({
persist = false,
packageManagers = [
{ command: (cmd) => convert(cmd, 'npm'), name: 'npm' },
{ command: (cmd) => convert(cmd, 'pnpm'), name: 'pnpm' },
{ command: (cmd) => convert(cmd, 'yarn'), name: 'yarn' },
{ command: (cmd) => convert(cmd, 'bun'), name: 'bun' },
],
}: RemarkNpmOptions = {}): Transformer<Root, Root> {
return (tree) => {
visit(tree, 'code', (node) => {
if (!node.lang || !aliases.includes(node.lang)) return 'skip';
const value =
node.value.startsWith('npm') || node.value.startsWith('npx')
? node.value
: `npm install ${node.value}`;
const attributes: MdxJsxAttribute[] = [
{
type: 'mdxJsxAttribute',
name: 'defaultValue',
value: packageManagers[0].name,
},
];
if (typeof persist === 'object') {
attributes.push(
{
type: 'mdxJsxAttribute',
name: 'groupId',
value: persist.id,
},
{
type: 'mdxJsxAttribute',
name: 'persist',
value: null,
},
);
}
const children: MdxJsxFlowElement[] = [
{
type: 'mdxJsxFlowElement',
name: 'CodeBlockTabsList',
attributes: [],
children: packageManagers.map(
({ name }) =>
({
type: 'mdxJsxFlowElement',
attributes: [
{ type: 'mdxJsxAttribute', name: 'value', value: name },
],
name: 'CodeBlockTabsTrigger',
children: [
{
type: 'text',
value: name,
} as unknown,
],
}) as MdxJsxFlowElement,
),
},
];
for (const { name, command } of packageManagers) {
children.push({
type: 'mdxJsxFlowElement',
name: 'CodeBlockTab',
attributes: [{ type: 'mdxJsxAttribute', name: 'value', value: name }],
children: [
{
type: 'code',
lang: 'bash',
meta: node.meta,
value: command(value),
},
],
});
}
const tab: MdxJsxFlowElement = {
type: 'mdxJsxFlowElement',
name: 'CodeBlockTabs',
attributes,
children,
};
Object.assign(node, tab);
return;
});
};
}
+33 -103
View File
@@ -3,7 +3,6 @@ import type { Transformer } from 'unified';
import { visit } from 'unist-util-visit';
import convert from 'npm-to-yarn';
import { createElement, expressionToAttribute } from './utils';
import { MdxJsxAttribute, MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
interface PackageManager {
name: string;
@@ -14,7 +13,10 @@ interface PackageManager {
command: (command: string) => string;
}
interface BaseOptions {
export type RemarkInstallOptions = {
Tabs?: string;
Tab?: string;
/**
* Persist Tab value (Fumadocs UI only)
*
@@ -27,19 +29,7 @@ interface BaseOptions {
| false;
packageManagers?: PackageManager[];
}
export type RemarkInstallOptions = BaseOptions &
(
| {
mode?: 'legacy';
Tabs?: string;
Tab?: string;
}
| {
mode: 'auto';
}
);
};
/**
* It generates the following structure from a code block with `package-install` as language
@@ -51,8 +41,12 @@ export type RemarkInstallOptions = BaseOptions &
* ...
* </Tabs>
* ```
*
* @deprecated Use `remarkNpm` from `fumadocs-core/mdx-plugins` instead, it's a drop-in replacement.
*/
export function remarkInstall({
Tab = 'Tab',
Tabs = 'Tabs',
persist = false,
packageManagers = [
{ command: (cmd) => convert(cmd, 'npm'), name: 'npm' },
@@ -60,105 +54,41 @@ export function remarkInstall({
{ command: (cmd) => convert(cmd, 'yarn'), name: 'yarn' },
{ command: (cmd) => convert(cmd, 'bun'), name: 'bun' },
],
...options
}: RemarkInstallOptions = {}): Transformer<Root, Root> {
return (tree) => {
visit(tree, 'code', (node) => {
if (node.lang !== 'package-install') return 'skip';
const value =
node.value.startsWith('npm') || node.value.startsWith('npx')
? node.value
: `npm install ${node.value}`;
const attributes: MdxJsxAttribute[] = [];
if (typeof persist === 'object') {
attributes.push(
{
type: 'mdxJsxAttribute',
name: 'groupId',
value: persist.id,
},
{
type: 'mdxJsxAttribute',
name: 'persist',
value: null,
},
);
}
if (options.mode === 'auto') {
attributes.push({
type: 'mdxJsxAttribute',
name: 'defaultValue',
value: packageManagers[0].name,
});
const children: MdxJsxFlowElement[] = [
{
type: 'mdxJsxFlowElement',
name: 'CodeBlockTabsList',
attributes: [],
children: packageManagers.map(
({ name }) =>
({
type: 'mdxJsxFlowElement',
attributes: [
{ type: 'mdxJsxAttribute', name: 'value', value: name },
],
name: 'CodeBlockTabsTrigger',
children: [
{
type: 'text',
value: name,
} as unknown,
],
}) as MdxJsxFlowElement,
),
},
...packageManagers.map(
({ command, name }) =>
({
type: 'mdxJsxFlowElement',
name: 'CodeBlockTab',
attributes: [
{ type: 'mdxJsxAttribute', name: 'value', value: name },
],
children: [
{
type: 'code',
lang: 'bash',
meta: node.meta,
value: command(value),
},
],
}) as MdxJsxFlowElement,
),
];
const tab: MdxJsxFlowElement = {
type: 'mdxJsxFlowElement',
name: 'CodeBlockTabs',
attributes,
children,
};
Object.assign(node, tab);
return;
}
const { Tabs = 'Tabs', Tab = 'Tab' } = options;
attributes.push(
expressionToAttribute('items', {
type: 'ArrayExpression',
elements: packageManagers.map(({ name }) => ({
type: 'Literal',
value: name,
})),
}),
);
const insert = createElement(
Tabs,
attributes,
[
...(typeof persist === 'object'
? [
{
type: 'mdxJsxAttribute',
name: 'groupId',
value: persist.id,
},
{
type: 'mdxJsxAttribute',
name: 'persist',
value: null,
},
]
: []),
expressionToAttribute('items', {
type: 'ArrayExpression',
elements: packageManagers.map(({ name }) => ({
type: 'Literal',
value: name,
})),
}),
],
packageManagers.map(({ command, name }) => ({
type: 'mdxJsxFlowElement',
name: Tab,
+6
View File
@@ -18,6 +18,7 @@ export type MDXOptions = Omit<
rehypeCodeOptions?: Plugins.RehypeCodeOptions | false;
rehypeTocOptions?: Plugins.RehypeTocOptions | false;
remarkCodeTabOptions?: Plugins.RemarkCodeTabOptions | false;
remarkNpmOptions?: Plugins.RemarkNpmOptions | false;
/**
* The directory to find image sizes
@@ -142,6 +143,7 @@ function getCompileOptions({
rehypeTocOptions,
remarkHeadingOptions,
remarkCodeTabOptions,
remarkNpmOptions,
imageDir = './public',
...options
}: MDXOptions = {}): CompileOptions {
@@ -153,6 +155,7 @@ function getCompileOptions({
const remarkGfm = getPlugin('remarkGfm');
const remarkHeading = getPlugin('remarkHeading');
const remarkCodeTab = getPlugin('remarkCodeTab');
const remarkNpm = getPlugin('remarkNpm');
const remarkImage = getPlugin('remarkImage');
const rehypeCode = getPlugin('rehypeCode');
const rehypeToc = getPlugin('rehypeToc');
@@ -179,6 +182,9 @@ function getCompileOptions({
remarkCodeTab && remarkCodeTabOptions !== false
? [remarkCodeTab, remarkCodeTabOptions]
: null,
remarkNpm && remarkNpmOptions !== false
? [remarkNpm, remarkNpmOptions]
: null,
...v,
],
options.remarkPlugins,
+4 -1
View File
@@ -22,6 +22,7 @@ export type DefaultMDXOptions = Omit<
remarkHeadingOptions?: plugins.RemarkHeadingOptions;
remarkImageOptions?: plugins.RemarkImageOptions | false;
remarkCodeTabOptions?: plugins.RemarkCodeTabOptions | false;
remarkNpmOptions?: plugins.RemarkNpmOptions | false;
rehypeCodeOptions?: plugins.RehypeCodeOptions | false;
};
@@ -47,6 +48,7 @@ export function getDefaultMDXOptions({
remarkHeadingOptions,
remarkStructureOptions,
remarkCodeTabOptions,
remarkNpmOptions,
...mdxOptions
}: DefaultMDXOptions): ProcessorOptions {
const mdxExports = [
@@ -67,12 +69,13 @@ export function getDefaultMDXOptions({
},
],
remarkImageOptions !== false && [plugins.remarkImage, remarkImageOptions],
// Fumadocs 14 compatibility
'remarkCodeTab' in plugins &&
remarkCodeTabOptions !== false && [
plugins.remarkCodeTab,
remarkCodeTabOptions,
],
'remarkNpm' in plugins &&
remarkNpmOptions !== false && [plugins.remarkNpm, remarkNpmOptions],
...v,
remarkStructureOptions !== false && [
plugins.remarkStructure,
+3
View File
@@ -796,6 +796,9 @@ importers:
negotiator:
specifier: ^1.0.0
version: 1.0.0
npm-to-yarn:
specifier: ^3.0.1
version: 3.0.1
react:
specifier: 18.x.x || 19.x.x
version: 19.1.0