mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
Migrate remarkDynamicContent and remarkInstall to docs generator
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
---
|
||||
"fumadocs-core": major
|
||||
"fumadocs-typescript": major
|
||||
---
|
||||
|
||||
**Introduce `fumadocs-docgen` package.**
|
||||
|
||||
Offer a better authoring experience for advanced use cases.
|
||||
|
||||
- Move `remark-dynamic-content` and `remark-install` plugins to the new package `fumadocs-docgen`.
|
||||
- Support Typescript generator by default
|
||||
|
||||
**Usage**
|
||||
|
||||
Add the `remarkDocGen` plugin to your remark plugins.
|
||||
|
||||
```ts
|
||||
import { remarkDocGen, fileGenerator } from "fumadocs-docgen";
|
||||
|
||||
remark().use(remarkDocGen, { generators: [fileGenerator()] })
|
||||
```
|
||||
|
||||
Generate docs with code blocks.
|
||||
|
||||
````mdx
|
||||
```json doc-gen:<generator>
|
||||
{
|
||||
// options
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
**Migrate**
|
||||
|
||||
For `remarkDynamicContent`, enable `fileGenerator` and use this syntax:
|
||||
|
||||
````mdx
|
||||
```json doc-gen:file
|
||||
{
|
||||
"file": "./path/to/my-file.txt"
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
For `remarkInstall`, it remains the same:
|
||||
|
||||
```ts
|
||||
import { remarkInstall } from "fumadocs-docgen"
|
||||
```
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"fumadocs-typescript": minor
|
||||
"fumadocs-core": minor
|
||||
---
|
||||
|
||||
Introduce Remark Docs Generation plugin
|
||||
@@ -97,8 +97,13 @@ Make sure to wrap your content in the [Page](/docs/ui/blocks/page) component, th
|
||||
<Accordions>
|
||||
<Accordion title="Example with Fumadocs MDX">
|
||||
|
||||
```tsx title="app/docs/[[...slug]]/page.tsx"
|
||||
|reference:../../examples/next-mdx/app/docs/[[...slug]]/page.tsx|
|
||||
```json doc-gen:file
|
||||
{
|
||||
"file": "../../examples/next-mdx/app/docs/[[...slug]]/page.tsx",
|
||||
"codeblock": {
|
||||
"meta": "title=\"app/docs/[[...slug]]/page.tsx\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
@@ -111,8 +116,13 @@ By default, it uses the built-in document search based on Flexsearch. [Learn to
|
||||
<Accordions>
|
||||
<Accordion title="Example with Fumadocs MDX">
|
||||
|
||||
```tsx title="app/api/search/route.ts"
|
||||
|reference:../../examples/next-mdx/app/api/search/route.ts|
|
||||
```json doc-gen:file
|
||||
{
|
||||
"file": "../../examples/next-mdx/app/api/search/route.ts",
|
||||
"codeblock": {
|
||||
"meta": "title=\"app/api/search/route.ts\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import createBundleAnalyzer from '@next/bundle-analyzer';
|
||||
import createMDX from 'fumadocs-mdx/config';
|
||||
import { rehypeCodeDefaultOptions } from 'fumadocs-core/mdx-plugins';
|
||||
import {
|
||||
remarkDynamicContent,
|
||||
remarkInstall,
|
||||
rehypeCodeDefaultOptions,
|
||||
remarkDocGen,
|
||||
} from 'fumadocs-core/mdx-plugins';
|
||||
remarkInstall,
|
||||
fileGenerator,
|
||||
typescriptGenerator,
|
||||
} from 'fumadocs-docgen';
|
||||
import { transformerTwoslash } from 'fumadocs-twoslash';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import { typescriptGen } from 'fumadocs-typescript';
|
||||
import remarkMath from 'remark-math';
|
||||
|
||||
const withAnalyzer = createBundleAnalyzer({
|
||||
@@ -63,9 +63,8 @@ const withMDX = createMDX({
|
||||
lastModifiedTime: 'git',
|
||||
remarkPlugins: [
|
||||
remarkMath,
|
||||
remarkDynamicContent,
|
||||
[remarkInstall, { Tabs: 'InstallTabs' }],
|
||||
[remarkDocGen, { generators: [typescriptGen()] }],
|
||||
[remarkDocGen, { generators: [typescriptGenerator(), fileGenerator()] }],
|
||||
],
|
||||
rehypePlugins: (v) => [rehypeKatex, ...v],
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"algoliasearch": "^4.23.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"fumadocs-core": "workspace:*",
|
||||
"fumadocs-docgen": "workspace:^",
|
||||
"fumadocs-mdx": "workspace:*",
|
||||
"fumadocs-twoslash": "workspace:^",
|
||||
"fumadocs-typescript": "workspace:^",
|
||||
|
||||
@@ -4,8 +4,5 @@ export {
|
||||
} from 'remark-gfm';
|
||||
export * from './rehype-code';
|
||||
export * from './remark-image';
|
||||
export * from './remark-dynamic-content';
|
||||
export * from './remark-structure';
|
||||
export * from './remark-heading';
|
||||
export * from './remark-install';
|
||||
export * from './remark-doc-gen';
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { type Code, type Root } from 'mdast';
|
||||
import { type Transformer } from 'unified';
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
export interface RemarkDocGenerator {
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Transform codeblocks to another mdast element
|
||||
*/
|
||||
run: (node: Code, options: unknown) => unknown;
|
||||
}
|
||||
|
||||
export interface RemarkDocGenOptions {
|
||||
generators?: RemarkDocGenerator[];
|
||||
}
|
||||
|
||||
const metaRegex = /doc-gen:(?<name>.+)/;
|
||||
|
||||
export function remarkDocGen({
|
||||
generators = [],
|
||||
}: RemarkDocGenOptions): Transformer<Root, Root> {
|
||||
return (tree) => {
|
||||
visit(tree, 'code', (c) => {
|
||||
if (c.lang !== 'json' || !c.meta) return;
|
||||
|
||||
const matches = metaRegex.exec(c.meta);
|
||||
if (!matches) return;
|
||||
|
||||
const name = matches[1];
|
||||
const gen = generators.find((g) => g.name === name);
|
||||
const result = gen?.run(c, JSON.parse(c.value));
|
||||
|
||||
if (result) Object.assign(c, result);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import type { Root } from 'mdast';
|
||||
import type { Transformer } from 'unified';
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
const regex = /^\|reference:(?<path>.+)\|/;
|
||||
|
||||
export interface RemarkDynamicContentOptions {
|
||||
/** @defaultValue true */
|
||||
trim?: boolean;
|
||||
|
||||
/**
|
||||
* Resolve reference files relative to `vfile.path`
|
||||
* @defaultValue false
|
||||
*/
|
||||
relative?: boolean;
|
||||
|
||||
/**
|
||||
* Filter specific element types
|
||||
* @defaultValue ['text','code']
|
||||
* */
|
||||
visit?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy content from referenced file
|
||||
*
|
||||
* @example
|
||||
* |reference:../path/to/file.ts|
|
||||
*/
|
||||
export function remarkDynamicContent(
|
||||
options: RemarkDynamicContentOptions = {},
|
||||
): Transformer<Root, Root> {
|
||||
const {
|
||||
trim = true,
|
||||
relative = false,
|
||||
visit: filter = ['text', 'code'],
|
||||
} = options;
|
||||
|
||||
return (tree, file) => {
|
||||
const cwd = file.cwd;
|
||||
|
||||
visit(tree, filter, (node) => {
|
||||
const canReplace = 'value' in node && typeof node.value === 'string';
|
||||
if (!canReplace) return;
|
||||
|
||||
const result = regex.exec(node.value);
|
||||
|
||||
if (result) {
|
||||
const dest = relative
|
||||
? path.resolve(cwd, path.dirname(file.path), result[1])
|
||||
: path.resolve(cwd, result[1]);
|
||||
let value = fs.readFileSync(dest).toString();
|
||||
if (trim) value = value.trim();
|
||||
|
||||
node.value = value;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
## Hello World
|
||||
|
||||
```md
|
||||
|reference:./fixtures/remark-dynamic-content.md|
|
||||
```
|
||||
@@ -1,9 +0,0 @@
|
||||
## Hello World
|
||||
|
||||
````md
|
||||
## Hello World
|
||||
|
||||
```md
|
||||
|reference:./fixtures/remark-dynamic-content.md|
|
||||
```
|
||||
````
|
||||
@@ -1,5 +0,0 @@
|
||||
## Hello World
|
||||
|
||||
```md
|
||||
|reference:./remark-dynamic-content.md|
|
||||
```
|
||||
@@ -1,9 +0,0 @@
|
||||
## Hello World
|
||||
|
||||
````md
|
||||
## Hello World
|
||||
|
||||
```md
|
||||
|reference:./fixtures/remark-dynamic-content.md|
|
||||
```
|
||||
````
|
||||
@@ -2,47 +2,11 @@ import { expect, test } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { remark } from 'remark';
|
||||
import {
|
||||
remarkDynamicContent,
|
||||
remarkHeading,
|
||||
remarkInstall,
|
||||
remarkStructure,
|
||||
} from '@/mdx-plugins';
|
||||
import { remarkHeading, remarkStructure } from '@/mdx-plugins';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createProcessor } from '@mdx-js/mdx';
|
||||
|
||||
const cwd = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
test('Remark Dynamic Content', async () => {
|
||||
const content = readFileSync(
|
||||
path.resolve(cwd, './fixtures/remark-dynamic-content.md'),
|
||||
);
|
||||
|
||||
const result = await remark()
|
||||
.use(remarkDynamicContent)
|
||||
.process({ value: content, cwd });
|
||||
|
||||
expect(result.toString()).toMatchFileSnapshot(
|
||||
path.resolve(cwd, './fixtures/remark-dynamic-content.output.md'),
|
||||
);
|
||||
});
|
||||
|
||||
test('Remark Dynamic Content - Relative', async () => {
|
||||
const file = path.resolve(
|
||||
cwd,
|
||||
'./fixtures/remark-dynamic-content.relative.md',
|
||||
);
|
||||
const content = readFileSync(file);
|
||||
|
||||
const result = await remark()
|
||||
.use(remarkDynamicContent, { relative: true })
|
||||
.process({ path: file, value: content });
|
||||
|
||||
expect(result.toString()).toMatchFileSnapshot(
|
||||
path.resolve(cwd, './fixtures/remark-dynamic-content.relative.output.md'),
|
||||
);
|
||||
});
|
||||
|
||||
test('Remark Heading', async () => {
|
||||
const file = path.resolve(cwd, './fixtures/remark-heading.md');
|
||||
const content = readFileSync(file);
|
||||
@@ -54,19 +18,6 @@ test('Remark Heading', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Remark Install', async () => {
|
||||
const file = path.resolve(cwd, './fixtures/remark-install.md');
|
||||
const content = readFileSync(file);
|
||||
|
||||
const result = await createProcessor({
|
||||
remarkPlugins: [remarkInstall],
|
||||
}).process(content);
|
||||
|
||||
expect(result.toString()).toMatchFileSnapshot(
|
||||
path.resolve(cwd, './fixtures/remark-install.output.js'),
|
||||
);
|
||||
});
|
||||
|
||||
test('Remark Structure', async () => {
|
||||
const content = readFileSync(
|
||||
path.resolve(cwd, './fixtures/remark-structure.md'),
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
extends: ['custom/library'],
|
||||
ignorePatterns: ['*.test.ts'],
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
# Fumadocs Doc Gen
|
||||
|
||||
Remark plugins & Docs Generator utilities.
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "fumadocs-docgen",
|
||||
"version": "1.0.0",
|
||||
"description": "Useful remark utilities and plugins",
|
||||
"keywords": [
|
||||
"NextJs",
|
||||
"fumadocs",
|
||||
"remark",
|
||||
"mdx",
|
||||
"Docs"
|
||||
],
|
||||
"homepage": "https://fumadocs.vercel.app",
|
||||
"repository": "github:fuma-nama/fumadocs",
|
||||
"license": "MIT",
|
||||
"author": "Fuma Nama",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"clean": "rimraf dist",
|
||||
"dev": "tsup --watch",
|
||||
"lint": "eslint .",
|
||||
"types:check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"estree-util-value-to-estree": "^3.1.1",
|
||||
"fumadocs-typescript": "workspace:^",
|
||||
"hast-util-to-estree": "^3.1.0",
|
||||
"npm-to-yarn": "^2.2.1",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mdx-js/mdx": "^3.0.1",
|
||||
"@types/estree": "^1.0.5",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/mdast": "^4.0.3",
|
||||
"@types/node": "18.17.5",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"remark": "^15.0.0",
|
||||
"tsconfig": "workspace:*",
|
||||
"unified": "^11.0.4"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import type { Code, Paragraph } from 'mdast';
|
||||
import type { DocGenerator } from './remark-docgen';
|
||||
|
||||
export interface FileGeneratorOptions {
|
||||
/** @defaultValue true */
|
||||
trim?: boolean;
|
||||
|
||||
/**
|
||||
* Resolve reference files relative to `vfile.path`
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
relative?: boolean;
|
||||
}
|
||||
|
||||
export interface FileGeneratorInput {
|
||||
file: string;
|
||||
|
||||
/**
|
||||
* Turn file content into a code block
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
codeblock?: CodeBlock | boolean;
|
||||
}
|
||||
|
||||
interface CodeBlock {
|
||||
lang?: string;
|
||||
meta?: string;
|
||||
}
|
||||
|
||||
export function fileGenerator({
|
||||
relative = false,
|
||||
trim = true,
|
||||
}: FileGeneratorOptions = {}): DocGenerator {
|
||||
return {
|
||||
name: 'file',
|
||||
run(input, ctx) {
|
||||
const { file, codeblock = false } = input as FileGeneratorInput;
|
||||
|
||||
const dest = relative
|
||||
? path.resolve(ctx.cwd, path.dirname(ctx.path), file)
|
||||
: path.resolve(ctx.cwd, file);
|
||||
let value = fs.readFileSync(dest).toString();
|
||||
if (trim) value = value.trim();
|
||||
|
||||
if (codeblock === false) {
|
||||
return {
|
||||
type: 'paragraph',
|
||||
children: [{ type: 'text', value }],
|
||||
} as Paragraph;
|
||||
}
|
||||
|
||||
const codeOptions = codeblock === true ? {} : codeblock;
|
||||
|
||||
return {
|
||||
type: 'code',
|
||||
lang: codeOptions.lang ?? path.extname(dest).slice(1),
|
||||
meta: codeOptions.meta,
|
||||
value,
|
||||
} as Code;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './remark-docgen';
|
||||
export * from './utils';
|
||||
export * from './file-generator';
|
||||
export * from './typescript-generator';
|
||||
export * from './remark-install';
|
||||
@@ -0,0 +1,49 @@
|
||||
import { type BlockContent, type Code, type Root } from 'mdast';
|
||||
import { type Transformer } from 'unified';
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
export interface DocGenerator {
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Transform codeblocks to another mdast element
|
||||
*/
|
||||
run: (input: unknown, context: Context) => object | undefined;
|
||||
}
|
||||
|
||||
interface Context {
|
||||
node: Code;
|
||||
path: string;
|
||||
cwd: string;
|
||||
}
|
||||
|
||||
export interface RemarkDocGenOptions {
|
||||
generators?: DocGenerator[];
|
||||
}
|
||||
|
||||
const metaRegex = /doc-gen:(?<name>.+)/;
|
||||
|
||||
export function remarkDocGen({
|
||||
generators = [],
|
||||
}: RemarkDocGenOptions): Transformer<Root, Root> {
|
||||
return (tree, file) => {
|
||||
visit(tree, 'code', (code, index, parent) => {
|
||||
if (code.lang !== 'json' || !code.meta) return;
|
||||
|
||||
const matches = metaRegex.exec(code.meta);
|
||||
if (!matches) return;
|
||||
|
||||
const name = matches[1];
|
||||
const gen = generators.find((g) => g.name === name);
|
||||
const result = gen?.run(JSON.parse(code.value), {
|
||||
cwd: file.cwd,
|
||||
path: file.path,
|
||||
node: code,
|
||||
});
|
||||
|
||||
if (result && typeof index === 'number' && parent) {
|
||||
parent.children[index] = result as BlockContent;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
+14
-29
@@ -2,6 +2,7 @@ import type { Code, Root } from 'mdast';
|
||||
import type { Transformer } from 'unified';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import convert from 'npm-to-yarn';
|
||||
import { createElement } from './utils';
|
||||
|
||||
interface PackageManager {
|
||||
name: string;
|
||||
@@ -47,35 +48,18 @@ export function remarkInstall({
|
||||
? node.value
|
||||
: `npm install ${node.value}`;
|
||||
|
||||
const insert = {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: Tabs,
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'items',
|
||||
value: {
|
||||
type: 'mdxJsxAttributeValueExpression',
|
||||
data: {
|
||||
estree: {
|
||||
body: [
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'ArrayExpression',
|
||||
elements: packageManagers.map(({ name }) => ({
|
||||
type: 'Literal',
|
||||
value: name,
|
||||
})),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
const insert = createElement(
|
||||
Tabs,
|
||||
{
|
||||
items: {
|
||||
type: 'ArrayExpression',
|
||||
elements: packageManagers.map(({ name }) => ({
|
||||
type: 'Literal',
|
||||
value: name,
|
||||
})),
|
||||
},
|
||||
],
|
||||
children: packageManagers.map(({ command, name }) => ({
|
||||
},
|
||||
packageManagers.map(({ command, name }) => ({
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: Tab,
|
||||
attributes: [{ type: 'mdxJsxAttribute', name: 'value', value: name }],
|
||||
@@ -83,11 +67,12 @@ export function remarkInstall({
|
||||
{
|
||||
type: 'code',
|
||||
lang: 'bash',
|
||||
meta: node.meta,
|
||||
value: command(value),
|
||||
} satisfies Code,
|
||||
],
|
||||
})),
|
||||
};
|
||||
);
|
||||
|
||||
Object.assign(node, insert);
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import * as path from 'node:path';
|
||||
import { valueToEstree } from 'estree-util-value-to-estree';
|
||||
import type { ExpressionStatement, ObjectExpression, Property } from 'estree';
|
||||
import { toEstree as hastToEstree } from 'hast-util-to-estree';
|
||||
import {
|
||||
type GenerateDocumentationOptions,
|
||||
generateDocumentation,
|
||||
renderMarkdownToHast,
|
||||
type DocEntry,
|
||||
} from 'fumadocs-typescript';
|
||||
import { createElement } from './utils';
|
||||
import type { DocGenerator } from './remark-docgen';
|
||||
|
||||
export type TypescriptGeneratorOptions = GenerateDocumentationOptions;
|
||||
|
||||
export interface TypescriptGeneratorInput {
|
||||
file: string;
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Component name which accepts the `type` property
|
||||
*
|
||||
* @defaultValue 'TypeTable'
|
||||
*/
|
||||
component?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Docs generator for Typescript
|
||||
*
|
||||
* @param options - configuration
|
||||
*/
|
||||
export function typescriptGenerator(
|
||||
options?: TypescriptGeneratorOptions,
|
||||
): DocGenerator {
|
||||
function mapProperty(entry: DocEntry): Property {
|
||||
const value = valueToEstree({
|
||||
type: entry.type,
|
||||
description: '',
|
||||
default: entry.tags.default || entry.tags.defaultValue,
|
||||
}) as ObjectExpression;
|
||||
|
||||
if (entry.description) {
|
||||
const hast = hastToEstree(renderMarkdownToHast(entry.description), {
|
||||
elementAttributeNameCase: 'react',
|
||||
}).body[0] as ExpressionStatement;
|
||||
|
||||
value.properties.push({
|
||||
type: 'Property',
|
||||
method: false,
|
||||
shorthand: false,
|
||||
computed: false,
|
||||
key: {
|
||||
type: 'Identifier',
|
||||
name: 'description',
|
||||
},
|
||||
kind: 'init',
|
||||
value: hast.expression,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Property',
|
||||
method: false,
|
||||
shorthand: false,
|
||||
computed: false,
|
||||
key: {
|
||||
type: 'Identifier',
|
||||
name: entry.name,
|
||||
},
|
||||
kind: 'init',
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'typescript',
|
||||
run(input, ctx) {
|
||||
const {
|
||||
file,
|
||||
name,
|
||||
component = 'TypeTable',
|
||||
} = input as TypescriptGeneratorInput;
|
||||
const dest = path.resolve(ctx.cwd, file);
|
||||
|
||||
const doc = generateDocumentation(dest, name, options);
|
||||
if (!doc) throw new Error(`Failed to find type ${name} in ${dest}`);
|
||||
|
||||
return createElement(component, {
|
||||
type: {
|
||||
type: 'ObjectExpression',
|
||||
properties: doc.entries.map(mapProperty),
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { Expression, Program } from 'estree';
|
||||
|
||||
export function createElement(
|
||||
name: string,
|
||||
properties: Record<string, Expression>,
|
||||
children?: unknown,
|
||||
): object {
|
||||
const element: Record<string, unknown> = {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name,
|
||||
attributes: Object.entries(properties).map(([key, prop]) => ({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: key,
|
||||
value: {
|
||||
type: 'mdxJsxAttributeValueExpression',
|
||||
data: {
|
||||
estree: {
|
||||
type: 'Program',
|
||||
body: [
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
expression: prop,
|
||||
},
|
||||
],
|
||||
} as Program,
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
if (children) element.children = children;
|
||||
|
||||
return element;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
## Hello World
|
||||
|
||||
```json doc-gen:file
|
||||
{
|
||||
"file": "./fixtures/sample.txt"
|
||||
}
|
||||
```
|
||||
|
||||
## Code Block
|
||||
|
||||
```json doc-gen:file
|
||||
{
|
||||
"file": "./fixtures/sample.ts",
|
||||
"codeblock": true
|
||||
}
|
||||
```
|
||||
|
||||
## Code Block with config
|
||||
|
||||
```json doc-gen:file
|
||||
{
|
||||
"file": "./fixtures/sample.txt",
|
||||
"codeblock": {
|
||||
"lang": "md",
|
||||
"meta": "title=\"Hello World\""
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
## Hello World
|
||||
|
||||
Hello World
|
||||
|
||||
This is some text
|
||||
|
||||
## Code Block
|
||||
|
||||
```ts
|
||||
export interface Test1 {
|
||||
name: string;
|
||||
description: string;
|
||||
isGood: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## Code Block with config
|
||||
|
||||
```md title="Hello World"
|
||||
Hello World
|
||||
|
||||
This is some text
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
## Hello World
|
||||
|
||||
```json doc-gen:file
|
||||
{
|
||||
"file": "./sample.txt",
|
||||
"codeblock": true
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
## Hello World
|
||||
|
||||
```txt
|
||||
Hello World
|
||||
|
||||
This is some text
|
||||
```
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
export interface Test1 {
|
||||
name: string;
|
||||
description: string;
|
||||
isGood: boolean;
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Hello World
|
||||
|
||||
This is some text
|
||||
@@ -0,0 +1,8 @@
|
||||
## Normal
|
||||
|
||||
```json doc-gen:typescript
|
||||
{
|
||||
"file": "./fixtures/sample.ts",
|
||||
"name": "Test1"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
|
||||
function _createMdxContent(props) {
|
||||
const _components = {
|
||||
h2: "h2",
|
||||
...props.components
|
||||
}, {TypeTable} = _components;
|
||||
if (!TypeTable) _missingMdxReference("TypeTable", true);
|
||||
return _jsxs(_Fragment, {
|
||||
children: [_jsx(_components.h2, {
|
||||
children: "Normal"
|
||||
}), "\n", _jsx(TypeTable, {
|
||||
type: {
|
||||
name: {
|
||||
"type": "string",
|
||||
"description": "",
|
||||
"default": undefined
|
||||
},
|
||||
description: {
|
||||
"type": "string",
|
||||
"description": "",
|
||||
"default": undefined
|
||||
},
|
||||
isGood: {
|
||||
"type": "boolean",
|
||||
"description": "",
|
||||
"default": undefined
|
||||
}
|
||||
}
|
||||
})]
|
||||
});
|
||||
}
|
||||
export default function MDXContent(props = {}) {
|
||||
const {wrapper: MDXLayout} = props.components || ({});
|
||||
return MDXLayout ? _jsx(MDXLayout, {
|
||||
...props,
|
||||
children: _jsx(_createMdxContent, {
|
||||
...props
|
||||
})
|
||||
}) : _createMdxContent(props);
|
||||
}
|
||||
function _missingMdxReference(id, component) {
|
||||
throw new Error("Expected " + (component ? "component" : "object") + " `" + id + "` to be defined: you likely forgot to import, pass, or provide it.");
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { remarkDocGen } from '@/remark-docgen';
|
||||
import { createProcessor } from '@mdx-js/mdx';
|
||||
import { fileGenerator } from '@/file-generator';
|
||||
import * as path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { remarkInstall } from '@/remark-install';
|
||||
import { remark } from 'remark';
|
||||
import { typescriptGenerator } from '@/typescript-generator';
|
||||
|
||||
const cwd = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
test('File Generator', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/file-gen.md'));
|
||||
|
||||
const processor = remark().use(remarkDocGen, {
|
||||
generators: [fileGenerator()],
|
||||
});
|
||||
|
||||
const result = await processor.process({ cwd, value: content });
|
||||
|
||||
expect(result.toString()).toMatchFileSnapshot(
|
||||
path.resolve(cwd, './fixtures/file-gen.output.md'),
|
||||
);
|
||||
});
|
||||
|
||||
test('File Generator - Relative', async () => {
|
||||
const file = path.resolve(cwd, './fixtures/file-gen.relative.md');
|
||||
const content = readFileSync(file);
|
||||
|
||||
const result = await remark()
|
||||
.use(remarkDocGen, {
|
||||
generators: [fileGenerator({ relative: true })],
|
||||
})
|
||||
.process({ path: file, value: content, cwd });
|
||||
|
||||
expect(result.toString()).toMatchFileSnapshot(
|
||||
path.resolve(cwd, './fixtures/file-gen.relative.output.md'),
|
||||
);
|
||||
});
|
||||
|
||||
test('Remark Install', async () => {
|
||||
const file = path.resolve(cwd, './fixtures/remark-install.md');
|
||||
const content = readFileSync(file);
|
||||
|
||||
const result = await createProcessor({
|
||||
remarkPlugins: [remarkInstall],
|
||||
}).process(content);
|
||||
|
||||
expect(result.toString()).toMatchFileSnapshot(
|
||||
path.resolve(cwd, './fixtures/remark-install.output.js'),
|
||||
);
|
||||
});
|
||||
|
||||
test('Typescript Generator', async () => {
|
||||
const file = path.resolve(cwd, './fixtures/typescript-gen.md');
|
||||
const content = readFileSync(file);
|
||||
|
||||
const tsconfig = {
|
||||
tsconfigPath: path.resolve(cwd, '../tsconfig.json'),
|
||||
basePath: path.resolve(cwd, '../'),
|
||||
};
|
||||
|
||||
const result = await createProcessor({
|
||||
remarkPlugins: [
|
||||
[
|
||||
remarkDocGen,
|
||||
{ generators: [typescriptGenerator({ config: tsconfig })] },
|
||||
],
|
||||
],
|
||||
}).process({ value: content, cwd });
|
||||
|
||||
expect(result.toString()).toMatchFileSnapshot(
|
||||
path.resolve(cwd, './fixtures/typescript-gen.output.js'),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "tsconfig/react-library.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
external: [],
|
||||
dts: true,
|
||||
target: 'es6',
|
||||
format: 'esm',
|
||||
entry: ['src/index.ts'],
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineProject } from 'vitest/config';
|
||||
|
||||
export default defineProject({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@/': new URL('./src/', import.meta.url).pathname,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -42,9 +42,7 @@
|
||||
"types:check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"estree-util-value-to-estree": "^3.1.1",
|
||||
"fast-glob": "^3.3.1",
|
||||
"hast-util-to-estree": "^3.1.0",
|
||||
"hast-util-to-jsx-runtime": "^2.3.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-gfm": "^3.0.0",
|
||||
@@ -57,7 +55,6 @@
|
||||
"@types/react": "18.2.67",
|
||||
"@types/react-dom": "18.2.22",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"fumadocs-core": "workspace:*",
|
||||
"fumadocs-ui": "workspace:*",
|
||||
"tsconfig": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './generate/base';
|
||||
export * from './generate/mdx';
|
||||
export * from './generate/file';
|
||||
export * from './plugins/remark-typescript-gen';
|
||||
export { renderMarkdownToHast } from './markdown';
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import { valueToEstree } from 'estree-util-value-to-estree';
|
||||
import type { RemarkDocGenerator } from 'fumadocs-core/mdx-plugins';
|
||||
import type { ExpressionStatement, ObjectExpression, Property } from 'estree';
|
||||
import { toEstree as hastToEstree } from 'hast-util-to-estree';
|
||||
import { renderMarkdownToHast } from '@/markdown';
|
||||
import {
|
||||
type GenerateDocumentationOptions,
|
||||
generateDocumentation,
|
||||
type DocEntry,
|
||||
} from '../generate/base';
|
||||
|
||||
export interface GeneratorOptions {
|
||||
file: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function typescriptGen(
|
||||
config?: GenerateDocumentationOptions,
|
||||
): RemarkDocGenerator {
|
||||
function mapProperty(entry: DocEntry): Property {
|
||||
const value = valueToEstree({
|
||||
type: entry.type,
|
||||
description: '',
|
||||
default: entry.tags.default || entry.tags.defaultValue,
|
||||
}) as ObjectExpression;
|
||||
|
||||
if (entry.description) {
|
||||
const hast = hastToEstree(renderMarkdownToHast(entry.description), {
|
||||
elementAttributeNameCase: 'react',
|
||||
}).body[0] as ExpressionStatement;
|
||||
|
||||
value.properties = value.properties.map((p) => {
|
||||
if (
|
||||
p.type === 'Property' &&
|
||||
p.key.type === 'Literal' &&
|
||||
p.key.value === 'description'
|
||||
) {
|
||||
return {
|
||||
...p,
|
||||
value: hast.expression as unknown,
|
||||
};
|
||||
}
|
||||
return p;
|
||||
}) as ObjectExpression['properties'];
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Property',
|
||||
method: false,
|
||||
shorthand: false,
|
||||
computed: false,
|
||||
key: {
|
||||
type: 'Identifier',
|
||||
name: entry.name,
|
||||
},
|
||||
kind: 'init',
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'typescript',
|
||||
run(_, options) {
|
||||
const { file, name } = options as GeneratorOptions;
|
||||
const doc = generateDocumentation(file, name, config);
|
||||
if (!doc) throw new Error(`Failed to find type ${name} in ${file}`);
|
||||
|
||||
return {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'TypeTable',
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'type',
|
||||
value: {
|
||||
type: 'mdxJsxAttributeValueExpression',
|
||||
data: {
|
||||
estree: {
|
||||
type: 'Program',
|
||||
body: [
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'ObjectExpression',
|
||||
properties: doc.entries.map(mapProperty),
|
||||
},
|
||||
} as ExpressionStatement,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Generated
+49
-11
@@ -70,6 +70,9 @@ importers:
|
||||
fumadocs-core:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/core
|
||||
fumadocs-docgen:
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/doc-gen
|
||||
fumadocs-mdx:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/mdx
|
||||
@@ -455,6 +458,52 @@ importers:
|
||||
specifier: ^5.4.5
|
||||
version: 5.4.5
|
||||
|
||||
packages/doc-gen:
|
||||
dependencies:
|
||||
estree-util-value-to-estree:
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.1
|
||||
fumadocs-typescript:
|
||||
specifier: workspace:^
|
||||
version: link:../typescript
|
||||
hast-util-to-estree:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0
|
||||
npm-to-yarn:
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1
|
||||
unist-util-visit:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
devDependencies:
|
||||
'@mdx-js/mdx':
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
'@types/estree':
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5
|
||||
'@types/hast':
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4
|
||||
'@types/mdast':
|
||||
specifier: ^4.0.3
|
||||
version: 4.0.3
|
||||
'@types/node':
|
||||
specifier: 18.17.5
|
||||
version: 18.17.5
|
||||
eslint-config-custom:
|
||||
specifier: workspace:*
|
||||
version: link:../eslint-config-custom
|
||||
remark:
|
||||
specifier: ^15.0.0
|
||||
version: 15.0.1
|
||||
tsconfig:
|
||||
specifier: workspace:*
|
||||
version: link:../tsconfig
|
||||
unified:
|
||||
specifier: ^11.0.4
|
||||
version: 11.0.4
|
||||
|
||||
packages/eslint-config-custom: {}
|
||||
|
||||
packages/mdx:
|
||||
@@ -575,15 +624,9 @@ importers:
|
||||
|
||||
packages/typescript:
|
||||
dependencies:
|
||||
estree-util-value-to-estree:
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.1
|
||||
fast-glob:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.2
|
||||
hast-util-to-estree:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0
|
||||
hast-util-to-jsx-runtime:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
@@ -618,9 +661,6 @@ importers:
|
||||
eslint-config-custom:
|
||||
specifier: workspace:*
|
||||
version: link:../eslint-config-custom
|
||||
fumadocs-core:
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
fumadocs-ui:
|
||||
specifier: workspace:*
|
||||
version: link:../ui
|
||||
@@ -9564,7 +9604,6 @@ packages:
|
||||
'@types/mdast': 4.0.3
|
||||
mdast-util-to-markdown: 2.1.0
|
||||
unified: 11.0.4
|
||||
dev: false
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==}
|
||||
@@ -9575,7 +9614,6 @@ packages:
|
||||
unified: 11.0.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==}
|
||||
|
||||
Reference in New Issue
Block a user