mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-28 07:09:50 +00:00
Finish the fumadocs→Hanzo Docs rebrand: rename the remaining fuma-named
workspace packages and every reference so the framework is uniformly
@hanzo/docs-*. One brand, no Fuma in package names or product surface.
Package renames (name field + all refs: deps, imports, CSS @import,
configs, .changeset, next/vite build config):
fumadocs-core → @hanzo/docs-core
fumadocs-mdx → @hanzo/docs-mdx
fumadocs-ui → @hanzo/docs-ui (packages/radix-ui)
fumadocs-openapi → @hanzo/docs-openapi
fumadocs-preview → @hanzo/docs-preview
@fumadocs/base-ui → @hanzo/docs-base-ui
@fumadocs/basehub → @hanzo/docs-basehub
@fumadocs/language → @hanzo/docs-language
@fumadocs/local-md → @hanzo/docs-local-md
@fumadocs/sanity → @hanzo/docs-sanity
@fumadocs/shadcn → @hanzo/docs-shadcn
@fumadocs/vite → @hanzo/docs-vite
@fumari/stf → @hanzo/docs-stf
create-fumadocs-app → @hanzo/create-docs
create-fumadocs-versions → @hanzo/docs-create-versions
Stale @fumadocs/{cli,story,tailwind,mdx-remote,...} references collapsed to
their existing @hanzo/docs-* canonical packages (no dup created).
De-branded product strings: Fumadocs/Fuma prose → Hanzo Docs/Hanzo; site
branding, CLI output, doc copy, AI-assistant name, metadata. URLs
fumadocs.dev → docs.hanzo.ai, repo fuma-nama/fumadocs → hanzoai/docs.
Filesystem renames: packages/python/fumapy → hanzodocs_py, preview config,
what-is-fumadocs.mdx → what-is-hanzo-docs.mdx, api/fumadocs-ui route.
KEPT (real external npm deps / attribution — NOT renamed):
fuma-cli, fuma-content, @fumari/json-schema-ts (published upstream pkgs),
typesense-/trieve-fumadocs-adapter (3rd-party search adapters),
LICENSE "Copyright (c) 2023 Fuma" (OSS attribution, all 7 LICENSE files
untouched), CHANGELOG.md history (accurate release records, left as-is).
Build: node 24, pnpm install clean; all 4 target apps next build GREEN
(spec, gui-docs, zen-docs, base-docs) + full package closure (18/18 turbo).
Gate: zero @hanzo-package fumadocs-/@fumadocs/ references remain.
312 lines
8.3 KiB
TypeScript
312 lines
8.3 KiB
TypeScript
import { mkdir, writeFile } from 'node:fs/promises';
|
|
import * as path from 'node:path';
|
|
import { generateDocument, type PagesToTextOptions, toText } from './utils/pages/to-text';
|
|
import type { DereferencedDocument } from '@/utils/document/dereference';
|
|
import type { OpenAPIServer } from '@/server';
|
|
import { createGetUrl, getSlugs, PathUtils } from '@hanzo/docs-core/source';
|
|
import { createAutoPreset, type SchemaToPagesOptions } from '@/utils/pages/preset-auto';
|
|
import { fromSchema, type OutputGroup, type OutputEntry } from '@/utils/pages/builder';
|
|
import type { DistributiveOmit } from './types';
|
|
import { doubleQuote } from './requests/string-utils';
|
|
|
|
export interface OutputFile {
|
|
path: string;
|
|
content: string;
|
|
}
|
|
|
|
interface IndexConfig {
|
|
items: IndexItem[] | ((ctx: BeforeWriteContext) => IndexItem[]);
|
|
|
|
/**
|
|
* Generate URLs for cards
|
|
*/
|
|
url:
|
|
| ((filePath: string) => string)
|
|
| {
|
|
baseUrl: string;
|
|
/**
|
|
* Base content directory
|
|
*/
|
|
contentDir: string;
|
|
};
|
|
}
|
|
|
|
interface IndexItem {
|
|
/** output path of index page */
|
|
path: string;
|
|
title?: string;
|
|
description?: string;
|
|
|
|
/**
|
|
* Specify linked pages:
|
|
* - items in `inputs` to include all generated pages of a specific schema.
|
|
* - file paths (using forward slash).
|
|
*/
|
|
only?: string[];
|
|
}
|
|
|
|
interface GenerateFilesConfig extends PagesToTextOptions {
|
|
/**
|
|
* the OpenAPI server object
|
|
*/
|
|
input: OpenAPIServer;
|
|
|
|
/**
|
|
* Output directory
|
|
*/
|
|
output: string;
|
|
|
|
/**
|
|
* Generate index files with cards linking to generated pages.
|
|
*/
|
|
index?: IndexConfig;
|
|
|
|
/**
|
|
* Generate `meta.json` files.
|
|
*
|
|
* Note: for flexibility, it's recommended to define them on your own.
|
|
*/
|
|
meta?: boolean | MetaOptions;
|
|
|
|
/**
|
|
* Can add/change/remove output files before writing to file system
|
|
**/
|
|
beforeWrite?: (this: BeforeWriteContext, files: OutputFile[]) => void | Promise<void>;
|
|
}
|
|
|
|
interface MetaOptions {
|
|
/** @deprecated use `folderStyle` instead */
|
|
groupStyle?: 'folder' | 'separator';
|
|
folderStyle?: 'folder' | 'separator';
|
|
}
|
|
|
|
export type Config = SchemaToPagesOptions &
|
|
GenerateFilesConfig & {
|
|
/**
|
|
* Re-generate when **schema files are changed**, ignores custom input functions & URLs.
|
|
*
|
|
* Note: it is recommended to configure & use `chokidar` on your own, this is only for simple cases.
|
|
*/
|
|
watch?: boolean;
|
|
};
|
|
|
|
interface BeforeWriteContext {
|
|
readonly generated: Record<string, OutputFile[]>;
|
|
readonly generatedEntries: Record<string, OutputEntry[]>;
|
|
readonly documents: Record<string, DereferencedDocument>;
|
|
}
|
|
|
|
export async function generateFiles(options: Config): Promise<void> {
|
|
if (options.watch) {
|
|
const { watch } = await import('chokidar');
|
|
const subOptions: Config = { ...options, watch: false };
|
|
|
|
await generateFiles(subOptions);
|
|
const targets = options.input._getWatchPaths();
|
|
console.log(`[Hanzo Docs OpenAPI] watching ${targets.join(', ')}`);
|
|
watch(targets, {
|
|
ignoreInitial: true,
|
|
}).on('all', () => generateFiles(subOptions));
|
|
return;
|
|
}
|
|
|
|
const files = await generateFilesOnly(options);
|
|
|
|
await Promise.all(
|
|
files.map(async (file) => {
|
|
const filePath = path.join(options.output, file.path);
|
|
|
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
await writeFile(filePath, file.content);
|
|
console.log(`Generated: ${filePath}`);
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function generateFilesOnly(
|
|
options: DistributiveOmit<Config, 'output'>,
|
|
): Promise<OutputFile[]> {
|
|
const schemas = await options.input.getSchemas();
|
|
|
|
const files: OutputFile[] = [];
|
|
const generated: Record<string, OutputFile[]> = {};
|
|
const generatedEntries: Record<string, OutputEntry[]> = {};
|
|
|
|
const entries = Object.entries(schemas);
|
|
if (entries.length === 0) {
|
|
throw new Error('No input files found.');
|
|
}
|
|
const preset = createAutoPreset(options);
|
|
for (const [id, schema] of entries) {
|
|
const entries = fromSchema(id, schema, preset);
|
|
const schemaFiles: OutputFile[] = [];
|
|
|
|
generatedEntries[id] = entries;
|
|
function scan(entry: OutputEntry) {
|
|
if (entry.type === 'group') {
|
|
for (const child of entry.entries) scan(child);
|
|
return;
|
|
}
|
|
|
|
schemaFiles.push({
|
|
path: entry.path,
|
|
content: toText(entry, schema, options),
|
|
});
|
|
}
|
|
|
|
for (const entry of entries) scan(entry);
|
|
generated[id] = schemaFiles;
|
|
files.push(...schemaFiles);
|
|
}
|
|
|
|
const context: BeforeWriteContext = {
|
|
generated,
|
|
generatedEntries,
|
|
documents: schemas,
|
|
};
|
|
|
|
if (options.index) {
|
|
files.push(...writeIndexFiles(context, options));
|
|
}
|
|
|
|
if (options.meta) {
|
|
files.push(...generateMeta(context, options.meta === true ? {} : options.meta));
|
|
}
|
|
|
|
await options.beforeWrite?.call(context, files);
|
|
return files;
|
|
}
|
|
|
|
function generateMeta(context: BeforeWriteContext, options: MetaOptions): OutputFile[] {
|
|
const files: OutputFile[] = [];
|
|
const folderStyle = options.folderStyle ?? options.groupStyle ?? 'folder';
|
|
|
|
function scan(entries: OutputEntry[], parent?: OutputGroup) {
|
|
const pages: string[] = [];
|
|
|
|
for (const entry of entries) {
|
|
const relativePath = PathUtils.slash(
|
|
parent ? path.relative(parent.path, entry.path) : entry.path,
|
|
);
|
|
|
|
if (entry.type === 'group') {
|
|
scan(entry.entries, entry);
|
|
|
|
if (folderStyle === 'folder') {
|
|
pages.push(relativePath);
|
|
} else {
|
|
pages.push(`---${entry.info.title}---`, `...${relativePath}`);
|
|
}
|
|
} else {
|
|
pages.push(relativePath.slice(0, -path.extname(entry.path).length));
|
|
}
|
|
}
|
|
|
|
if (pages.length === 0) return;
|
|
files.push({
|
|
path: parent ? path.join(parent.path, 'meta.json') : 'meta.json',
|
|
content: JSON.stringify(
|
|
{
|
|
title: parent?.info.title,
|
|
description: parent?.info.description,
|
|
pages,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
});
|
|
}
|
|
|
|
for (const entries of Object.values(context.generatedEntries)) {
|
|
scan(entries);
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
function writeIndexFiles(
|
|
context: BeforeWriteContext,
|
|
options: SchemaToPagesOptions & Omit<GenerateFilesConfig, 'output'>,
|
|
): OutputFile[] {
|
|
const files: OutputFile[] = [];
|
|
const { generatedEntries } = context;
|
|
const pathToEntry = new Map<string, OutputEntry>();
|
|
const { items, url } = options.index!;
|
|
|
|
function indexEntry(entry: OutputEntry) {
|
|
pathToEntry.set(PathUtils.slash(entry.path), entry);
|
|
if (entry.type === 'group') {
|
|
for (const child of entry.entries) {
|
|
indexEntry(child);
|
|
}
|
|
}
|
|
}
|
|
|
|
let urlFn: (path: string) => string;
|
|
if (typeof url === 'object') {
|
|
const getUrl = createGetUrl(url.baseUrl);
|
|
|
|
urlFn = (file) => getUrl(getSlugs(path.relative(url.contentDir, file)));
|
|
} else {
|
|
urlFn = url;
|
|
}
|
|
|
|
function fileContent(index: IndexItem): string {
|
|
const content: string[] = [];
|
|
content.push('<Cards>');
|
|
const outputEntries: OutputEntry[] = [];
|
|
const only = index.only ?? Object.keys(context.generated);
|
|
|
|
for (const item of only) {
|
|
if (generatedEntries[item]) {
|
|
for (const entry of generatedEntries[item]) {
|
|
outputEntries.push(entry);
|
|
}
|
|
} else {
|
|
const match = pathToEntry.get(item);
|
|
if (!match) {
|
|
throw new Error(
|
|
`${item} does not exist on "input", available: ${Object.keys(generatedEntries).join(', ')}.`,
|
|
);
|
|
}
|
|
|
|
outputEntries.push(match);
|
|
}
|
|
}
|
|
|
|
for (const entry of outputEntries) {
|
|
// cannot link to groups
|
|
if (entry.type === 'group') continue;
|
|
const descriptionAttr = entry.info.description
|
|
? `description=${doubleQuote(entry.info.description)} `
|
|
: '';
|
|
content.push(
|
|
`<Card href="${urlFn(entry.path)}" title=${doubleQuote(entry.info.title)} ${descriptionAttr}/>`,
|
|
);
|
|
}
|
|
|
|
content.push('</Cards>');
|
|
return generateDocument(
|
|
{
|
|
title: index.title ?? 'Overview',
|
|
description: index.description,
|
|
},
|
|
content.join('\n'),
|
|
options,
|
|
);
|
|
}
|
|
|
|
for (const list of Object.values(context.generatedEntries)) {
|
|
for (const item of list) indexEntry(item);
|
|
}
|
|
|
|
for (const item of typeof items === 'function' ? items(context) : items) {
|
|
files.push({
|
|
path: path.extname(item.path).length === 0 ? `${item.path}.mdx` : item.path,
|
|
content: fileContent(item),
|
|
});
|
|
}
|
|
|
|
return files;
|
|
}
|