CLI: Support slot in customise command

This commit is contained in:
Fuma Nama
2026-03-22 00:45:18 +08:00
parent 37e0243bd4
commit 823d880752
4 changed files with 323 additions and 61 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@fumadocs/cli': patch
---
Support slot in customise command
+7 -7
View File
@@ -104,6 +104,7 @@ export class RegistryCompiler {
name: registry.name,
info: {
indexes: [],
unlistedIndexes: [],
env: registry.env,
variables: registry.variables,
},
@@ -119,14 +120,13 @@ export class RegistryCompiler {
);
for (const [input, comp] of builtComps) {
if (!input.unlisted) {
output.info.indexes.push({
name: input.name,
title: input.title,
description: input.description,
});
}
const arr = input.unlisted ? output.info.unlistedIndexes : output.info.indexes;
arr.push({
name: input.name,
title: input.title,
description: input.description,
});
output.components.push(comp);
}
+310 -54
View File
@@ -8,7 +8,15 @@ import { pluginPreserveLayouts } from '@/registry/plugins/preserve';
interface TargetInfo {
target: string[];
replace: [string, string][];
id: string;
print?: () => void;
}
interface SlotPrintInfo {
at: string;
layoutId: string;
name: string;
isPage: boolean;
}
export async function customise(client: RegistryClient) {
@@ -18,79 +26,138 @@ export async function customise(client: RegistryClient) {
plugins: [pluginPreserveLayouts()],
});
const registry = UIRegistries[config.uiLibrary];
const info = await client.createLinkedRegistryClient(registry).fetchRegistryInfo();
const result = await group(
{
layout: () =>
layout: (): Promise<TargetInfo | symbol> =>
select({
message: 'What do you want to customise?',
options: [
{
label: 'Docs Layout',
value: 'docs',
hint: 'main UI of your docs',
value: {
id: 'docs',
target: [`${registry}/layouts/docs`],
print() {
printLayout(
['fumadocs-ui/layouts/docs', '@/layouts/docs'],
['fumadocs-ui/layouts/docs/page', '@/layouts/docs/page'],
);
},
},
hint: 'the default docs layout',
},
{
label: 'Notebook Layout',
value: {
id: 'notebook',
target: [`${registry}/layouts/notebook`],
print() {
printLayout(
['fumadocs-ui/layouts/notebook', '@/layouts/notebook'],
['fumadocs-ui/layouts/notebook/page', '@/layouts/notebook/page'],
);
},
},
hint: 'a more compact version of docs layout',
},
{
label: 'Flux Layout',
value: {
id: 'flux',
target: [`${registry}/layouts/flux`],
print() {
printLayout(
['fumadocs-ui/layouts/flux', '@/layouts/flux'],
['fumadocs-ui/layouts/flux/page', '@/layouts/flux/page'],
);
},
},
hint: 'the experimental variant of docs layout',
},
{
label: 'Home Layout',
value: 'home',
hint: 'the navbar for your other pages',
value: {
id: 'home',
target: [`${registry}/layouts/home`],
print() {
printLayout(['fumadocs-ui/layouts/home', `@/layouts/home`]);
},
},
hint: 'the layout for other non-docs pages',
},
],
}),
target: (v): Promise<TargetInfo | symbol> => {
if (v.results.layout !== 'docs')
return Promise.resolve({
target: [`${registry}/layouts/home`],
replace: [['fumadocs-ui/layouts/home', `@/components/layout/home`]],
});
const selected = v.results.layout!;
if (selected.id === 'home') return Promise.resolve(selected);
return select<TargetInfo>({
message: 'Which variant do you want to start from?',
message: 'Which part do you want to customise?',
options: [
{
label: 'Start from minimal styles',
hint: 'for those who want to build their own variant from ground up.',
label: 'All',
hint: 'install the entire layout',
value: selected,
},
{
label: 'Replace & rewrite from minimal styles',
hint: 'for those who want to build their own UI from ground up',
value: {
id: 'docs-min',
target: ['layouts/docs-min'],
replace: [
['fumadocs-ui/layouts/docs', '@/components/layout/docs'],
['fumadocs-ui/layouts/docs/page', '@/components/layout/docs/page'],
],
print() {
printLayout(
['fumadocs-ui/layouts/docs', '@/layouts/docs'],
['fumadocs-ui/layouts/docs/page', '@/layouts/docs/page'],
);
},
},
},
{
label: 'Start from default layout',
value: {
target: [`${registry}/layouts/docs`],
replace: [
['fumadocs-ui/layouts/docs', '@/components/layout/docs'],
['fumadocs-ui/layouts/docs/page', '@/components/layout/docs/page'],
],
},
hint: 'useful for adjusting small details.',
},
{
label: 'Start from Notebook layout',
value: {
target: [`${registry}/layouts/notebook`],
replace: [
['fumadocs-ui/layouts/notebook', '@/components/layout/notebook'],
['fumadocs-ui/layouts/notebook/page', '@/components/layout/notebook/page'],
],
},
hint: 'useful for adjusting small details.',
},
{
label: 'Start from Flux layout',
value: {
target: [`${registry}/layouts/flux`],
replace: [
['fumadocs-ui/layouts/flux', '@/components/layout/flux'],
['fumadocs-ui/layouts/flux/page', '@/components/layout/flux/page'],
],
},
hint: 'useful for adjusting small details.',
},
...info.unlistedIndexes.flatMap((index) => {
const prefix = `slots/${selected.id}`;
if (!index.name.startsWith(prefix)) return [];
let name = index.name.slice(prefix.length + 1);
if (name.startsWith('page/')) {
name = name.slice('page/'.length);
return {
label: `Page: ${name}`,
hint: "only replace a part of layout's page, useful for adjusting details",
value: {
id: index.name,
target: [`${registry}/${index.name}`],
print() {
printSlot({
at: `@/layouts/${selected.id}/page/slots/${name}`,
layoutId: selected.id,
name,
isPage: true,
});
},
} as TargetInfo,
};
}
return {
label: `Layout: ${name}`,
hint: 'only replace a part of layout, useful for adjusting details',
value: {
id: index.name,
target: [`${registry}/${index.name}`],
print() {
printSlot({
at: `@/layouts/${selected.id}/slots/${name}`,
layoutId: selected.id,
name,
isPage: false,
});
},
} as TargetInfo,
};
}),
],
});
},
@@ -105,20 +172,209 @@ export async function customise(client: RegistryClient) {
const target = result.target as TargetInfo;
await install(target.target, installer);
printNext(...target.replace);
target.print?.();
outro(picocolors.bold('Have fun!'));
}
function printNext(...maps: [from: string, to: string][]) {
function printLayout(...maps: [from: string, to: string][]) {
intro(picocolors.bold('What is Next?'));
log.info(
[
'You can check the installed components in `components`.',
'You can check the installed layouts in `layouts` folder.',
picocolors.dim('---'),
'Open your `layout.tsx` files, replace the imports of components:',
...maps.map(([from, to]) => picocolors.greenBright(`"${from}" -> "${to}"`)),
].join('\n'),
);
}
function printSlot({ at, layoutId, name, isPage }: SlotPrintInfo) {
intro(picocolors.bold('What is Next?'));
log.info(`You can check the installed layout slot in "${at}".`);
const code = getSlotCode({ at, layoutId, name, isPage });
if (code) {
if (isPage) {
log.info(
`${picocolors.bold('At your <DocsPage /> component, update your "slots" prop:')}\n\n${code}`,
);
} else {
log.info(
`${picocolors.bold('At your <DocsLayout /> component, update your "slots" prop:')}\n\n${code}`,
);
}
}
}
function getSlotCode({ at, layoutId, name, isPage }: SlotPrintInfo): string | undefined {
if (isPage) {
switch (name) {
case 'toc':
if (layoutId === 'flux') {
return `import { TOCProvider, TOC } from '${at}';
return (
<DocsPage
slots={{
toc: {
provider: TOCProvider,
main: TOC,
},
}}
>
...
</DocsPage>
);`;
}
return `import { TOCProvider, TOC, TOCPopover } from '${at}';
return (
<DocsPage
slots={{
toc: {
provider: TOCProvider,
main: TOC,
popover: TOCPopover,
},
}}
>
...
</DocsPage>
);`;
case 'container': {
return `import { Container } from '${at}';
return (
<DocsPage
slots={{
container: Container,
}}
>
...
</DocsPage>
);`;
}
case 'footer': {
return `import { Footer } from '${at}';
return (
<DocsPage
slots={{
footer: Footer,
}}
>
...
</DocsPage>
);`;
}
case 'breadcrumb': {
return `import { Breadcrumb } from '${at}';
return (
<DocsPage
slots={{
breadcrumb: Breadcrumb,
}}
>
...
</DocsPage>
);`;
}
default:
return;
}
}
switch (name) {
case 'sidebar': {
if (layoutId === 'notebook') {
return `import {
SidebarProvider,
Sidebar,
SidebarTrigger,
SidebarCollapseTrigger,
useSidebar,
} from '${at}';
return (
<DocsLayout
slots={{
sidebar: {
provider: SidebarProvider,
root: Sidebar,
trigger: SidebarTrigger,
collapseTrigger: SidebarCollapseTrigger,
useSidebar: useSidebar,
},
}}
>
...
</DocsLayout>
);`;
}
return `import { SidebarProvider, Sidebar, SidebarTrigger, useSidebar } from '${at}';
return (
<DocsLayout
slots={{
sidebar: {
provider: SidebarProvider,
root: Sidebar,
trigger: SidebarTrigger,
useSidebar: useSidebar,
},
}}
>
...
</DocsLayout>
);`;
}
case 'container': {
return `import { Container } from '${at}';
return (
<DocsLayout
slots={{
container: Container,
}}
>
...
</DocsLayout>
);`;
}
case 'header': {
return `import { Header } from '${at}';
return (
<DocsLayout
slots={{
header: Header,
}}
>
...
</DocsLayout>
);`;
}
case 'tab-dropdown': {
return `import { TabDropdown } from '${at}';
return (
<DocsLayout
slots={{
tabDropdown: TabDropdown,
}}
>
...
</DocsLayout>
);`;
}
default:
return;
}
}
+1
View File
@@ -66,6 +66,7 @@ export const registryInfoSchema = z.object({
*/
env: z.record(z.string(), z.unknown()).optional(),
indexes: z.array(indexSchema).default([]),
unlistedIndexes: z.array(indexSchema).default([]),
registries: z.array(z.string()).optional(),
});