mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core: Support handling duplicated slugs and conflicts such as dir/index.mdx vs dir.mdx
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': patch
|
||||
---
|
||||
|
||||
Support handling duplicated slugs and conflicts such as `dir/index.mdx` vs `dir.mdx`
|
||||
@@ -5,14 +5,7 @@ import { VirtualFile } from '@/source/loader';
|
||||
|
||||
export interface LoadOptions {
|
||||
transformers?: Transformer[];
|
||||
}
|
||||
|
||||
export interface I18nLoadOptions extends LoadOptions {
|
||||
i18n: {
|
||||
parser: 'dot' | 'dir';
|
||||
languages: string[];
|
||||
defaultLanguage: string;
|
||||
};
|
||||
buildFiles: (files: VirtualFile[]) => (MetaFile | PageFile)[];
|
||||
}
|
||||
|
||||
export type ContentStorage = FileSystem<MetaFile | PageFile>;
|
||||
@@ -42,22 +35,17 @@ export type Transformer = (context: {
|
||||
// Virtual files -> Virtual Storage -> Transformers -> Result
|
||||
export function loadFiles(
|
||||
files: VirtualFile[],
|
||||
buildFile: (file: VirtualFile) => MetaFile | PageFile,
|
||||
options: LoadOptions,
|
||||
): ContentStorage {
|
||||
const { transformers = [] } = options;
|
||||
const storage: ContentStorage = new FileSystem();
|
||||
const normalized: VirtualFile[] = files.map((file) => ({
|
||||
...file,
|
||||
path: normalizePath(file.path),
|
||||
}));
|
||||
|
||||
for (const file of files) {
|
||||
const parsedPath = normalizePath(file.path);
|
||||
|
||||
storage.write(
|
||||
parsedPath,
|
||||
buildFile({
|
||||
...file,
|
||||
path: parsedPath,
|
||||
}),
|
||||
);
|
||||
for (const item of options.buildFiles(normalized)) {
|
||||
storage.write(item.path, item);
|
||||
}
|
||||
|
||||
for (const transformer of transformers) {
|
||||
@@ -72,18 +60,22 @@ export function loadFiles(
|
||||
|
||||
export function loadFilesI18n(
|
||||
files: VirtualFile[],
|
||||
buildFile: (file: VirtualFile) => MetaFile | PageFile,
|
||||
options: I18nLoadOptions,
|
||||
options: LoadOptions,
|
||||
i18n: {
|
||||
parser: 'dot' | 'dir';
|
||||
languages: string[];
|
||||
defaultLanguage: string;
|
||||
},
|
||||
): Record<string, ContentStorage> {
|
||||
const parser = options.i18n.parser === 'dir' ? dirParser : dotParser;
|
||||
const parser = i18n.parser === 'dir' ? dirParser : dotParser;
|
||||
const storages: Record<string, ContentStorage> = {};
|
||||
|
||||
for (const lang of options.i18n.languages) {
|
||||
for (const lang of i18n.languages) {
|
||||
storages[lang] = loadFiles(
|
||||
files.flatMap((file) => {
|
||||
const [path, locale] = parser(normalizePath(file.path));
|
||||
|
||||
if ((locale ?? options.i18n.defaultLanguage) === lang) {
|
||||
if ((locale ?? i18n.defaultLanguage) === lang) {
|
||||
return {
|
||||
...file,
|
||||
path,
|
||||
@@ -92,7 +84,6 @@ export function loadFilesI18n(
|
||||
|
||||
return [];
|
||||
}),
|
||||
buildFile,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,13 @@ import {
|
||||
type BuildPageTreeOptions,
|
||||
createPageTreeBuilder,
|
||||
} from './page-tree-builder';
|
||||
import { type FileInfo, parseFilePath } from './path';
|
||||
import {
|
||||
basename,
|
||||
dirname,
|
||||
extname,
|
||||
type FileInfo,
|
||||
parseFilePath,
|
||||
} from './path';
|
||||
import { joinPath } from '@/utils/path';
|
||||
|
||||
export interface LoaderConfig {
|
||||
@@ -269,24 +275,6 @@ export function createGetUrl(baseUrl: string, i18n?: I18nConfig): UrlFn {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert file path into slugs, also encode non-ASCII characters, so they can work in pathname
|
||||
*/
|
||||
export function getSlugs(info: FileInfo): string[] {
|
||||
const slugs: string[] = [];
|
||||
|
||||
for (const seg of info.dirname.split('/')) {
|
||||
// filter empty names and file groups like (group_name)
|
||||
if (seg.length > 0 && !/^\(.+\)$/.test(seg)) slugs.push(encodeURI(seg));
|
||||
}
|
||||
|
||||
if (info.name !== 'index') {
|
||||
slugs.push(encodeURI(info.name));
|
||||
}
|
||||
|
||||
return slugs;
|
||||
}
|
||||
|
||||
export function loader<
|
||||
Config extends SourceConfig,
|
||||
I18n extends I18nConfig | undefined = undefined,
|
||||
@@ -305,43 +293,83 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
|
||||
console.warn('`loader()` now requires a `baseUrl` option to be defined.');
|
||||
}
|
||||
|
||||
const { source, slugs: slugsFn = getSlugs, i18n } = options;
|
||||
const {
|
||||
source,
|
||||
baseUrl = '/',
|
||||
i18n,
|
||||
slugs: slugsFn,
|
||||
url: getUrl = createGetUrl(baseUrl ?? '/', i18n),
|
||||
transformers,
|
||||
} = options;
|
||||
const defaultLanguage = i18n?.defaultLanguage ?? '';
|
||||
const getUrl =
|
||||
options.url ?? createGetUrl(options.baseUrl ?? '/', options.i18n);
|
||||
|
||||
const files =
|
||||
typeof source.files === 'function' ? source.files() : source.files;
|
||||
|
||||
function buildFile(file: VirtualFile): MetaFile | PageFile {
|
||||
if (file.type === 'page') {
|
||||
return {
|
||||
format: 'page',
|
||||
path: file.path,
|
||||
slugs: file.slugs ?? slugsFn(parseFilePath(file.path)),
|
||||
data: file.data as PageData,
|
||||
absolutePath: file.absolutePath ?? '',
|
||||
};
|
||||
function buildFiles(files: VirtualFile[]) {
|
||||
const indexFiles: VirtualFile[] = [];
|
||||
const taken = new Set<string>();
|
||||
|
||||
for (const file of files) {
|
||||
if (file.type !== 'page' || file.slugs) continue;
|
||||
|
||||
// for custom slugs function, don't handle conflicting cases like `dir/index.mdx` vs `dir.mdx`
|
||||
if (isIndex(file.path) && !slugsFn) {
|
||||
indexFiles.push(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
file.slugs = slugsFn
|
||||
? slugsFn(parseFilePath(file.path))
|
||||
: getSlugs(file.path);
|
||||
|
||||
const key = file.slugs.join('/');
|
||||
if (taken.has(key)) throw new Error('Duplicated slugs');
|
||||
taken.add(key);
|
||||
}
|
||||
|
||||
return {
|
||||
format: 'meta',
|
||||
path: file.path,
|
||||
absolutePath: file.absolutePath ?? '',
|
||||
data: file.data as MetaData,
|
||||
};
|
||||
for (const file of indexFiles) {
|
||||
file.slugs = getSlugs(file.path);
|
||||
|
||||
if (taken.has(file.slugs.join('/'))) file.slugs.push('index');
|
||||
}
|
||||
|
||||
return files.map((file) => {
|
||||
if (file.type === 'page') {
|
||||
return {
|
||||
format: 'page',
|
||||
path: file.path,
|
||||
slugs: file.slugs!,
|
||||
data: file.data,
|
||||
absolutePath: file.absolutePath ?? '',
|
||||
} as PageFile;
|
||||
}
|
||||
|
||||
return {
|
||||
format: 'meta',
|
||||
path: file.path,
|
||||
absolutePath: file.absolutePath ?? '',
|
||||
data: file.data,
|
||||
} as MetaFile;
|
||||
});
|
||||
}
|
||||
|
||||
const storages = i18n
|
||||
? loadFilesI18n(files, buildFile, {
|
||||
...options,
|
||||
i18n: {
|
||||
? loadFilesI18n(
|
||||
files,
|
||||
{
|
||||
buildFiles,
|
||||
transformers,
|
||||
},
|
||||
{
|
||||
...i18n,
|
||||
parser: i18n.parser ?? 'dot',
|
||||
},
|
||||
})
|
||||
)
|
||||
: {
|
||||
'': loadFiles(files, buildFile, options),
|
||||
'': loadFiles(files, {
|
||||
transformers,
|
||||
buildFiles,
|
||||
}),
|
||||
};
|
||||
|
||||
const walker = indexPages(storages, getUrl, i18n);
|
||||
@@ -372,7 +400,6 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
|
||||
pageTree = v;
|
||||
},
|
||||
getPageByHref(href, { dir = '', language } = {}) {
|
||||
const pages = this.getPages(language);
|
||||
const [value, hash] = href.split('#', 2);
|
||||
let target;
|
||||
|
||||
@@ -380,17 +407,18 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
|
||||
value.startsWith('.') &&
|
||||
(value.endsWith('.md') || value.endsWith('.mdx'))
|
||||
) {
|
||||
const hrefPath = joinPath(dir, value);
|
||||
target = pages.find((item) => item.file.path === hrefPath);
|
||||
const path = joinPath(dir, value);
|
||||
|
||||
target = walker.pathToPage.get(`${language}.${path}`);
|
||||
} else {
|
||||
target = pages.find((item) => item.url === value);
|
||||
target = this.getPages(language).find((item) => item.url === value);
|
||||
}
|
||||
|
||||
if (!target) return;
|
||||
return {
|
||||
page: target,
|
||||
hash,
|
||||
};
|
||||
if (target)
|
||||
return {
|
||||
page: target,
|
||||
hash,
|
||||
};
|
||||
},
|
||||
getPages(language = defaultLanguage) {
|
||||
const pages: Page[] = [];
|
||||
@@ -484,3 +512,34 @@ function fileToPage<Data = PageData>(
|
||||
locale,
|
||||
};
|
||||
}
|
||||
|
||||
const GroupRegex = /^\(.+\)$/;
|
||||
|
||||
function isIndex(file: string) {
|
||||
return basename(file, extname(file)) === 'index';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert file path into slugs, also encode non-ASCII characters, so they can work in pathname
|
||||
*/
|
||||
export function getSlugs(file: string | FileInfo): string[] {
|
||||
if (typeof file !== 'string') return getSlugs(file.path);
|
||||
|
||||
const dir = dirname(file);
|
||||
const name = basename(file, extname(file));
|
||||
const slugs: string[] = [];
|
||||
|
||||
for (const seg of dir.split('/')) {
|
||||
// filter empty names and file groups like (group_name)
|
||||
if (seg.length > 0 && !GroupRegex.test(seg)) slugs.push(encodeURI(seg));
|
||||
}
|
||||
|
||||
if (GroupRegex.test(name))
|
||||
throw new Error(`Cannot use folder group in file names: ${file}`);
|
||||
|
||||
if (name !== 'index') {
|
||||
slugs.push(encodeURI(name));
|
||||
}
|
||||
|
||||
return slugs;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$id": "root",
|
||||
"children": [
|
||||
{
|
||||
"$id": "test.mdx",
|
||||
"name": "Hello",
|
||||
"type": "page",
|
||||
"url": "/test",
|
||||
},
|
||||
],
|
||||
"name": "",
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { Source, SourceConfig } from '@/source';
|
||||
|
||||
export const source: Source<SourceConfig> = {
|
||||
files: [
|
||||
{
|
||||
type: 'page',
|
||||
path: 'test.mdx',
|
||||
data: {
|
||||
title: 'Hello',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: 'meta.json',
|
||||
data: {
|
||||
pages: ['test'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"cn": {
|
||||
"$id": "cn",
|
||||
"children": [
|
||||
{
|
||||
"$id": "test.mdx",
|
||||
"$ref": {
|
||||
"file": "test.mdx",
|
||||
},
|
||||
"name": "Hello Chinese",
|
||||
"type": "page",
|
||||
"url": "/cn/test",
|
||||
},
|
||||
{
|
||||
"$id": "nested",
|
||||
"children": [
|
||||
{
|
||||
"$id": "nested/test.mdx",
|
||||
"$ref": {
|
||||
"file": "nested/test.mdx",
|
||||
},
|
||||
"name": "Nested Page Chinese",
|
||||
"type": "page",
|
||||
"url": "/cn/nested/test",
|
||||
},
|
||||
],
|
||||
"name": "Nested",
|
||||
"type": "folder",
|
||||
},
|
||||
],
|
||||
"name": "Docs Chinese",
|
||||
},
|
||||
"en": {
|
||||
"$id": "en",
|
||||
"children": [
|
||||
{
|
||||
"$id": "test.mdx",
|
||||
"$ref": {
|
||||
"file": "test.mdx",
|
||||
},
|
||||
"name": "Hello",
|
||||
"type": "page",
|
||||
"url": "/en/test",
|
||||
},
|
||||
{
|
||||
"$id": "nested",
|
||||
"children": [
|
||||
{
|
||||
"$id": "nested/test.mdx",
|
||||
"$ref": {
|
||||
"file": "nested/test.mdx",
|
||||
},
|
||||
"name": "Nested Page",
|
||||
"type": "page",
|
||||
"url": "/en/nested/test",
|
||||
},
|
||||
],
|
||||
"name": "Nested",
|
||||
"type": "folder",
|
||||
},
|
||||
],
|
||||
"name": "Docs English",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { Source, SourceConfig } from '@/source';
|
||||
|
||||
export const source: Source<SourceConfig> = {
|
||||
files: [
|
||||
{
|
||||
type: 'page',
|
||||
path: 'en/test.mdx',
|
||||
data: {
|
||||
title: 'Hello',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: 'cn/test.mdx',
|
||||
data: {
|
||||
title: 'Hello Chinese',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: 'en/meta.json',
|
||||
data: {
|
||||
title: 'Docs English',
|
||||
pages: ['test', 'nested'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: 'cn/meta.json',
|
||||
data: {
|
||||
title: 'Docs Chinese',
|
||||
pages: ['test', 'nested'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: 'en/nested/test.mdx',
|
||||
data: {
|
||||
title: 'Nested Page',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: 'cn/nested/test.mdx',
|
||||
data: {
|
||||
title: 'Nested Page Chinese',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: 'cn/nested/meta.json',
|
||||
data: {
|
||||
title: 'Nested Chinese',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"cn": {
|
||||
"$id": "cn",
|
||||
"children": [
|
||||
{
|
||||
"$id": "test.mdx",
|
||||
"name": "Hello Chinese",
|
||||
"type": "page",
|
||||
"url": "/cn/test",
|
||||
},
|
||||
{
|
||||
"$id": "nested",
|
||||
"children": [
|
||||
{
|
||||
"$id": "nested/test.mdx",
|
||||
"name": "Nested Page Chinese",
|
||||
"type": "page",
|
||||
"url": "/cn/nested/test",
|
||||
},
|
||||
],
|
||||
"name": "Nested",
|
||||
"type": "folder",
|
||||
},
|
||||
],
|
||||
"name": "Docs Chinese",
|
||||
},
|
||||
"en": {
|
||||
"$id": "en",
|
||||
"children": [
|
||||
{
|
||||
"$id": "test.mdx",
|
||||
"name": "Hello",
|
||||
"type": "page",
|
||||
"url": "/test",
|
||||
},
|
||||
{
|
||||
"$id": "nested",
|
||||
"children": [
|
||||
{
|
||||
"$id": "nested/test.mdx",
|
||||
"name": "Nested Page",
|
||||
"type": "page",
|
||||
"url": "/nested/test",
|
||||
},
|
||||
],
|
||||
"name": "Nested",
|
||||
"type": "folder",
|
||||
},
|
||||
],
|
||||
"name": "Docs English",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
[
|
||||
{
|
||||
"language": "cn",
|
||||
"pages": [
|
||||
{
|
||||
"absolutePath": "",
|
||||
"data": {
|
||||
"title": "Hello Chinese",
|
||||
},
|
||||
"file": {
|
||||
"dirname": "",
|
||||
"ext": ".mdx",
|
||||
"flattenedPath": "test",
|
||||
"name": "test",
|
||||
"path": "test.mdx",
|
||||
},
|
||||
"locale": "cn",
|
||||
"path": "test.mdx",
|
||||
"slugs": [
|
||||
"test",
|
||||
],
|
||||
"url": "/cn/test",
|
||||
},
|
||||
{
|
||||
"absolutePath": "",
|
||||
"data": {
|
||||
"title": "Nested Page Chinese",
|
||||
},
|
||||
"file": {
|
||||
"dirname": "nested",
|
||||
"ext": ".mdx",
|
||||
"flattenedPath": "nested/test",
|
||||
"name": "test",
|
||||
"path": "nested/test.mdx",
|
||||
},
|
||||
"locale": "cn",
|
||||
"path": "nested/test.mdx",
|
||||
"slugs": [
|
||||
"nested",
|
||||
"test",
|
||||
],
|
||||
"url": "/cn/nested/test",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"language": "en",
|
||||
"pages": [
|
||||
{
|
||||
"absolutePath": "",
|
||||
"data": {
|
||||
"title": "Hello",
|
||||
},
|
||||
"file": {
|
||||
"dirname": "",
|
||||
"ext": ".mdx",
|
||||
"flattenedPath": "test",
|
||||
"name": "test",
|
||||
"path": "test.mdx",
|
||||
},
|
||||
"locale": "en",
|
||||
"path": "test.mdx",
|
||||
"slugs": [
|
||||
"test",
|
||||
],
|
||||
"url": "/en/test",
|
||||
},
|
||||
{
|
||||
"absolutePath": "",
|
||||
"data": {
|
||||
"title": "Nested Page",
|
||||
},
|
||||
"file": {
|
||||
"dirname": "nested",
|
||||
"ext": ".mdx",
|
||||
"flattenedPath": "nested/test",
|
||||
"name": "test",
|
||||
"path": "nested/test.mdx",
|
||||
},
|
||||
"locale": "en",
|
||||
"path": "nested/test.mdx",
|
||||
"slugs": [
|
||||
"nested",
|
||||
"test",
|
||||
],
|
||||
"url": "/en/nested/test",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"cn": {
|
||||
"$id": "cn",
|
||||
"children": [
|
||||
{
|
||||
"$id": "test.mdx",
|
||||
"$ref": {
|
||||
"file": "test.mdx",
|
||||
},
|
||||
"name": "Hello Chinese",
|
||||
"type": "page",
|
||||
"url": "/cn/test",
|
||||
},
|
||||
{
|
||||
"$id": "nested",
|
||||
"children": [
|
||||
{
|
||||
"$id": "nested/test.mdx",
|
||||
"$ref": {
|
||||
"file": "nested/test.mdx",
|
||||
},
|
||||
"name": "Nested Page Chinese",
|
||||
"type": "page",
|
||||
"url": "/cn/nested/test",
|
||||
},
|
||||
],
|
||||
"name": "Nested",
|
||||
"type": "folder",
|
||||
},
|
||||
],
|
||||
"name": "Docs Chinese",
|
||||
},
|
||||
"en": {
|
||||
"$id": "en",
|
||||
"children": [
|
||||
{
|
||||
"$id": "test.mdx",
|
||||
"$ref": {
|
||||
"file": "test.mdx",
|
||||
},
|
||||
"name": "Hello",
|
||||
"type": "page",
|
||||
"url": "/en/test",
|
||||
},
|
||||
{
|
||||
"$id": "nested",
|
||||
"children": [
|
||||
{
|
||||
"$id": "nested/test.mdx",
|
||||
"$ref": {
|
||||
"file": "nested/test.mdx",
|
||||
},
|
||||
"name": "Nested Page",
|
||||
"type": "page",
|
||||
"url": "/en/nested/test",
|
||||
},
|
||||
],
|
||||
"name": "Nested",
|
||||
"type": "folder",
|
||||
},
|
||||
],
|
||||
"name": "Docs English",
|
||||
},
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import type { Source, SourceConfig } from '@/source';
|
||||
|
||||
export const source: Source<SourceConfig> = {
|
||||
files: [
|
||||
{
|
||||
type: 'page',
|
||||
path: 'test.mdx',
|
||||
data: {
|
||||
title: 'Hello',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: 'test.cn.mdx',
|
||||
data: {
|
||||
title: 'Hello Chinese',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: 'meta.en.json',
|
||||
data: {
|
||||
title: 'Docs English',
|
||||
pages: ['test', 'nested'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: 'meta.cn.json',
|
||||
data: {
|
||||
title: 'Docs Chinese',
|
||||
pages: ['test', 'nested'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: '/nested/test.en.mdx',
|
||||
data: {
|
||||
title: 'Nested Page',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: '/nested/test.cn.mdx',
|
||||
data: {
|
||||
title: 'Nested Page Chinese',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: '/nested/meta.cn.json',
|
||||
data: {
|
||||
title: 'Nested Chinese',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"$id": "root",
|
||||
"children": [
|
||||
{
|
||||
"$id": "test.mdx",
|
||||
"name": "Hello",
|
||||
"type": "page",
|
||||
"url": "/test",
|
||||
},
|
||||
{
|
||||
"$id": "(nested)",
|
||||
"children": [
|
||||
{
|
||||
"$id": "(nested)/hello.mdx",
|
||||
"name": "Route Group Page",
|
||||
"type": "page",
|
||||
"url": "/hello",
|
||||
},
|
||||
],
|
||||
"name": "Nested",
|
||||
"type": "folder",
|
||||
},
|
||||
{
|
||||
"$id": "hello",
|
||||
"children": [],
|
||||
"index": {
|
||||
"$id": "hello/index.mdx",
|
||||
"name": "Another hello page",
|
||||
"type": "page",
|
||||
"url": "/hello/index",
|
||||
},
|
||||
"name": "Another hello page",
|
||||
"type": "folder",
|
||||
},
|
||||
{
|
||||
"$id": "nested",
|
||||
"children": [
|
||||
{
|
||||
"$id": "nested/test.mdx",
|
||||
"name": "Nested Page",
|
||||
"type": "page",
|
||||
"url": "/nested/test",
|
||||
},
|
||||
],
|
||||
"name": "Nested",
|
||||
"type": "folder",
|
||||
},
|
||||
{
|
||||
"external": true,
|
||||
"name": "Text",
|
||||
"type": "page",
|
||||
"url": "https://google.com",
|
||||
},
|
||||
{
|
||||
"external": true,
|
||||
"icon": "Icon",
|
||||
"name": "Text",
|
||||
"type": "page",
|
||||
"url": "https://google.com",
|
||||
},
|
||||
],
|
||||
"name": "",
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { Source, SourceConfig } from '@/source';
|
||||
|
||||
export const source: Source<SourceConfig> = {
|
||||
files: [
|
||||
{
|
||||
type: 'page',
|
||||
path: 'test.mdx',
|
||||
data: {
|
||||
title: 'Hello',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: 'hidden.mdx',
|
||||
data: {
|
||||
title: 'Hidden',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: 'meta.json',
|
||||
data: {
|
||||
pages: [
|
||||
'...',
|
||||
'!hidden',
|
||||
'nested',
|
||||
'[Text](https://google.com)',
|
||||
'[Icon][Text](https://google.com)',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: '/hello/index.mdx',
|
||||
data: {
|
||||
title: 'Another hello page',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: '/nested/test.mdx',
|
||||
data: {
|
||||
title: 'Nested Page',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: '/(nested)/hello.mdx',
|
||||
data: {
|
||||
title: 'Route Group Page',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,13 +1,6 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import {
|
||||
createGetUrl,
|
||||
getSlugs,
|
||||
loader,
|
||||
type Source,
|
||||
type SourceConfig,
|
||||
} from '@/source/loader';
|
||||
import { createGetUrl, getSlugs, loader } from '@/source/loader';
|
||||
import { parseFilePath } from '@/source';
|
||||
import { PageTree } from '../dist/server';
|
||||
import type { ReactElement } from 'react';
|
||||
import { removeUndefined } from '@/utils/remove-undefined';
|
||||
|
||||
@@ -29,10 +22,7 @@ test('get slugs: folder groups', () => {
|
||||
'page',
|
||||
]);
|
||||
|
||||
expect(getSlugs(parseFilePath('nested/(page).mdx'))).toStrictEqual([
|
||||
'nested',
|
||||
'(page)',
|
||||
]);
|
||||
expect(() => getSlugs(parseFilePath('nested/(page).mdx'))).toThrowError();
|
||||
});
|
||||
|
||||
test('Get URL: Empty', () => {
|
||||
@@ -48,47 +38,18 @@ test('Get URL: Base', () => {
|
||||
expect(getUrl([''])).toBe('/docs');
|
||||
});
|
||||
|
||||
test('Loader: Simple', () => {
|
||||
test('Loader: Simple', async () => {
|
||||
const result = loader({
|
||||
baseUrl: '/',
|
||||
pageTree: {
|
||||
noRef: true,
|
||||
},
|
||||
source: {
|
||||
files: [
|
||||
{
|
||||
type: 'page',
|
||||
path: 'test.mdx',
|
||||
data: {
|
||||
title: 'Hello',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: 'meta.json',
|
||||
data: {
|
||||
pages: ['test'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
source: (await import('./fixtures/page-trees/basic')).source,
|
||||
});
|
||||
|
||||
expect(removeUndefined(result.pageTree, true), 'Page Tree')
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"$id": "root",
|
||||
"children": [
|
||||
{
|
||||
"$id": "test.mdx",
|
||||
"name": "Hello",
|
||||
"type": "page",
|
||||
"url": "/test",
|
||||
},
|
||||
],
|
||||
"name": "",
|
||||
}
|
||||
`);
|
||||
await expect(removeUndefined(result.pageTree, true)).toMatchFileSnapshot(
|
||||
'./fixtures/page-trees/basic.tree.json',
|
||||
);
|
||||
|
||||
expect(result.getPages().length).toBe(1);
|
||||
expect(result.getPage(['test'])).toBeDefined();
|
||||
@@ -101,108 +62,23 @@ test('Nested Directories', async () => {
|
||||
pageTree: {
|
||||
noRef: true,
|
||||
},
|
||||
source: {
|
||||
files: [
|
||||
{
|
||||
type: 'page',
|
||||
path: 'test.mdx',
|
||||
data: {
|
||||
title: 'Hello',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: 'hidden.mdx',
|
||||
data: {
|
||||
title: 'Hidden',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: 'meta.json',
|
||||
data: {
|
||||
pages: [
|
||||
'...',
|
||||
'!hidden',
|
||||
'nested',
|
||||
'[Text](https://google.com)',
|
||||
'[Icon][Text](https://google.com)',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: '/nested/test.mdx',
|
||||
data: {
|
||||
title: 'Nested Page',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: '/(nested)/hello.mdx',
|
||||
data: {
|
||||
title: 'Route Group Page',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
source: (await import('./fixtures/page-trees/nested')).source,
|
||||
});
|
||||
|
||||
expect(removeUndefined(result.pageTree, true), 'Page Tree')
|
||||
await expect(removeUndefined(result.pageTree, true)).toMatchFileSnapshot(
|
||||
'./fixtures/page-trees/nested.tree.json',
|
||||
);
|
||||
|
||||
expect(result.getPages().map((page) => page.slugs.join('/')))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"$id": "root",
|
||||
"children": [
|
||||
{
|
||||
"$id": "test.mdx",
|
||||
"name": "Hello",
|
||||
"type": "page",
|
||||
"url": "/test",
|
||||
},
|
||||
{
|
||||
"$id": "(nested)",
|
||||
"children": [
|
||||
{
|
||||
"$id": "(nested)/hello.mdx",
|
||||
"name": "Route Group Page",
|
||||
"type": "page",
|
||||
"url": "/hello",
|
||||
},
|
||||
],
|
||||
"name": "Nested",
|
||||
"type": "folder",
|
||||
},
|
||||
{
|
||||
"$id": "nested",
|
||||
"children": [
|
||||
{
|
||||
"$id": "nested/test.mdx",
|
||||
"name": "Nested Page",
|
||||
"type": "page",
|
||||
"url": "/nested/test",
|
||||
},
|
||||
],
|
||||
"name": "Nested",
|
||||
"type": "folder",
|
||||
},
|
||||
{
|
||||
"external": true,
|
||||
"name": "Text",
|
||||
"type": "page",
|
||||
"url": "https://google.com",
|
||||
},
|
||||
{
|
||||
"external": true,
|
||||
"icon": "Icon",
|
||||
"name": "Text",
|
||||
"type": "page",
|
||||
"url": "https://google.com",
|
||||
},
|
||||
],
|
||||
"name": "",
|
||||
}
|
||||
[
|
||||
"test",
|
||||
"hidden",
|
||||
"hello/index",
|
||||
"nested/test",
|
||||
"hello",
|
||||
]
|
||||
`);
|
||||
expect(result.getPages().length).toBe(4);
|
||||
// page in folder
|
||||
expect(result.getPage(['nested', 'test'])).toBeDefined();
|
||||
|
||||
@@ -210,259 +86,25 @@ test('Nested Directories', async () => {
|
||||
expect(result.getPage(['hello'])).toBeDefined();
|
||||
});
|
||||
|
||||
const i18nSource: Source<SourceConfig> = {
|
||||
files: [
|
||||
{
|
||||
type: 'page',
|
||||
path: 'test.mdx',
|
||||
data: {
|
||||
title: 'Hello',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: 'test.cn.mdx',
|
||||
data: {
|
||||
title: 'Hello Chinese',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: 'meta.en.json',
|
||||
data: {
|
||||
title: 'Docs English',
|
||||
pages: ['test', 'nested'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: 'meta.cn.json',
|
||||
data: {
|
||||
title: 'Docs Chinese',
|
||||
pages: ['test', 'nested'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: '/nested/test.en.mdx',
|
||||
data: {
|
||||
title: 'Nested Page',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: '/nested/test.cn.mdx',
|
||||
data: {
|
||||
title: 'Nested Page Chinese',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: '/nested/meta.cn.json',
|
||||
data: {
|
||||
title: 'Nested Chinese',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
test('Internationalized Routing', () => {
|
||||
test('Internationalized Routing', async () => {
|
||||
const result = loader({
|
||||
baseUrl: '/',
|
||||
i18n: {
|
||||
languages: ['cn', 'en'],
|
||||
defaultLanguage: 'en',
|
||||
},
|
||||
source: i18nSource,
|
||||
source: (await import('./fixtures/page-trees/i18n')).source,
|
||||
});
|
||||
|
||||
expect(
|
||||
result.getNodePage(result.pageTree['en'].children[0] as PageTree.Item),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
title: 'Hello',
|
||||
},
|
||||
}),
|
||||
await expect(removeUndefined(result.pageTree, true)).toMatchFileSnapshot(
|
||||
'./fixtures/page-trees/i18n.tree.json',
|
||||
);
|
||||
expect(
|
||||
result.getNodePage(
|
||||
result.pageTree['en'].children[0] as PageTree.Item,
|
||||
'cn',
|
||||
),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
title: 'Hello Chinese',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(removeUndefined(result.pageTree['en'], true), 'Page Tree')
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"$id": "en",
|
||||
"children": [
|
||||
{
|
||||
"$id": "test.mdx",
|
||||
"$ref": {
|
||||
"file": "test.mdx",
|
||||
},
|
||||
"name": "Hello",
|
||||
"type": "page",
|
||||
"url": "/en/test",
|
||||
},
|
||||
{
|
||||
"$id": "nested",
|
||||
"children": [
|
||||
{
|
||||
"$id": "nested/test.mdx",
|
||||
"$ref": {
|
||||
"file": "nested/test.mdx",
|
||||
},
|
||||
"name": "Nested Page",
|
||||
"type": "page",
|
||||
"url": "/en/nested/test",
|
||||
},
|
||||
],
|
||||
"name": "Nested",
|
||||
"type": "folder",
|
||||
},
|
||||
],
|
||||
"name": "Docs English",
|
||||
}
|
||||
`);
|
||||
|
||||
expect(removeUndefined(result.pageTree['cn'], true), 'Page Tree')
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"$id": "cn",
|
||||
"children": [
|
||||
{
|
||||
"$id": "test.mdx",
|
||||
"$ref": {
|
||||
"file": "test.mdx",
|
||||
},
|
||||
"name": "Hello Chinese",
|
||||
"type": "page",
|
||||
"url": "/cn/test",
|
||||
},
|
||||
{
|
||||
"$id": "nested",
|
||||
"children": [
|
||||
{
|
||||
"$id": "nested/test.mdx",
|
||||
"$ref": {
|
||||
"file": "nested/test.mdx",
|
||||
},
|
||||
"name": "Nested Page Chinese",
|
||||
"type": "page",
|
||||
"url": "/cn/nested/test",
|
||||
},
|
||||
],
|
||||
"name": "Nested",
|
||||
"type": "folder",
|
||||
},
|
||||
],
|
||||
"name": "Docs Chinese",
|
||||
}
|
||||
`);
|
||||
|
||||
expect(result.getLanguages()).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"language": "cn",
|
||||
"pages": [
|
||||
{
|
||||
"absolutePath": "",
|
||||
"data": {
|
||||
"title": "Hello Chinese",
|
||||
},
|
||||
"file": {
|
||||
"dirname": "",
|
||||
"ext": ".mdx",
|
||||
"flattenedPath": "test",
|
||||
"name": "test",
|
||||
"path": "test.mdx",
|
||||
},
|
||||
"locale": "cn",
|
||||
"path": "test.mdx",
|
||||
"slugs": [
|
||||
"test",
|
||||
],
|
||||
"url": "/cn/test",
|
||||
},
|
||||
{
|
||||
"absolutePath": "",
|
||||
"data": {
|
||||
"title": "Nested Page Chinese",
|
||||
},
|
||||
"file": {
|
||||
"dirname": "nested",
|
||||
"ext": ".mdx",
|
||||
"flattenedPath": "nested/test",
|
||||
"name": "test",
|
||||
"path": "nested/test.mdx",
|
||||
},
|
||||
"locale": "cn",
|
||||
"path": "nested/test.mdx",
|
||||
"slugs": [
|
||||
"nested",
|
||||
"test",
|
||||
],
|
||||
"url": "/cn/nested/test",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"language": "en",
|
||||
"pages": [
|
||||
{
|
||||
"absolutePath": "",
|
||||
"data": {
|
||||
"title": "Hello",
|
||||
},
|
||||
"file": {
|
||||
"dirname": "",
|
||||
"ext": ".mdx",
|
||||
"flattenedPath": "test",
|
||||
"name": "test",
|
||||
"path": "test.mdx",
|
||||
},
|
||||
"locale": "en",
|
||||
"path": "test.mdx",
|
||||
"slugs": [
|
||||
"test",
|
||||
],
|
||||
"url": "/en/test",
|
||||
},
|
||||
{
|
||||
"absolutePath": "",
|
||||
"data": {
|
||||
"title": "Nested Page",
|
||||
},
|
||||
"file": {
|
||||
"dirname": "nested",
|
||||
"ext": ".mdx",
|
||||
"flattenedPath": "nested/test",
|
||||
"name": "test",
|
||||
"path": "nested/test.mdx",
|
||||
},
|
||||
"locale": "en",
|
||||
"path": "nested/test.mdx",
|
||||
"slugs": [
|
||||
"nested",
|
||||
"test",
|
||||
],
|
||||
"url": "/en/nested/test",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
await expect(
|
||||
removeUndefined(result.getLanguages(), true),
|
||||
).toMatchFileSnapshot('./fixtures/page-trees/i18n.entries.json');
|
||||
});
|
||||
|
||||
test('Internationalized Routing: Hide Prefix', () => {
|
||||
test('Internationalized Routing: Hide Prefix', async () => {
|
||||
const result = loader({
|
||||
baseUrl: '/',
|
||||
i18n: {
|
||||
@@ -473,72 +115,33 @@ test('Internationalized Routing: Hide Prefix', () => {
|
||||
pageTree: {
|
||||
noRef: true,
|
||||
},
|
||||
source: i18nSource,
|
||||
source: (await import('./fixtures/page-trees/i18n')).source,
|
||||
});
|
||||
|
||||
expect(removeUndefined(result.pageTree['en'], true), 'Page Tree')
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"$id": "en",
|
||||
"children": [
|
||||
{
|
||||
"$id": "test.mdx",
|
||||
"name": "Hello",
|
||||
"type": "page",
|
||||
"url": "/test",
|
||||
},
|
||||
{
|
||||
"$id": "nested",
|
||||
"children": [
|
||||
{
|
||||
"$id": "nested/test.mdx",
|
||||
"name": "Nested Page",
|
||||
"type": "page",
|
||||
"url": "/nested/test",
|
||||
},
|
||||
],
|
||||
"name": "Nested",
|
||||
"type": "folder",
|
||||
},
|
||||
],
|
||||
"name": "Docs English",
|
||||
}
|
||||
`);
|
||||
|
||||
expect(removeUndefined(result.pageTree['cn'], true), 'Page Tree')
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"$id": "cn",
|
||||
"children": [
|
||||
{
|
||||
"$id": "test.mdx",
|
||||
"name": "Hello Chinese",
|
||||
"type": "page",
|
||||
"url": "/cn/test",
|
||||
},
|
||||
{
|
||||
"$id": "nested",
|
||||
"children": [
|
||||
{
|
||||
"$id": "nested/test.mdx",
|
||||
"name": "Nested Page Chinese",
|
||||
"type": "page",
|
||||
"url": "/cn/nested/test",
|
||||
},
|
||||
],
|
||||
"name": "Nested",
|
||||
"type": "folder",
|
||||
},
|
||||
],
|
||||
"name": "Docs Chinese",
|
||||
}
|
||||
`);
|
||||
|
||||
await expect(removeUndefined(result.pageTree, true)).toMatchFileSnapshot(
|
||||
'./fixtures/page-trees/i18n-no-prefix.tree.json',
|
||||
);
|
||||
expect(result.getPages().length).toBe(2);
|
||||
expect(result.getPage(['test'])?.url).toBe('/test');
|
||||
expect(result.getPage(['test'], 'cn')?.url).toBe('/cn/test');
|
||||
});
|
||||
|
||||
test('Internationalized Routing: dir', async () => {
|
||||
const result = loader({
|
||||
baseUrl: '/',
|
||||
i18n: {
|
||||
parser: 'dir',
|
||||
languages: ['cn', 'en'],
|
||||
defaultLanguage: 'en',
|
||||
},
|
||||
source: (await import('./fixtures/page-trees/i18n-dir')).source,
|
||||
});
|
||||
|
||||
await expect(removeUndefined(result.pageTree, true)).toMatchFileSnapshot(
|
||||
'./fixtures/page-trees/i18n-dir.tree.json',
|
||||
);
|
||||
});
|
||||
|
||||
test('Loader: Without meta.json', () => {
|
||||
const result = loader({
|
||||
baseUrl: '/',
|
||||
@@ -649,140 +252,3 @@ test('Loader: Rest operator', () => {
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
const i18nSource2: Source<SourceConfig> = {
|
||||
files: [
|
||||
{
|
||||
type: 'page',
|
||||
path: 'en/test.mdx',
|
||||
data: {
|
||||
title: 'Hello',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: 'cn/test.mdx',
|
||||
data: {
|
||||
title: 'Hello Chinese',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: 'en/meta.json',
|
||||
data: {
|
||||
title: 'Docs English',
|
||||
pages: ['test', 'nested'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: 'cn/meta.json',
|
||||
data: {
|
||||
title: 'Docs Chinese',
|
||||
pages: ['test', 'nested'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: 'en/nested/test.mdx',
|
||||
data: {
|
||||
title: 'Nested Page',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: 'cn/nested/test.mdx',
|
||||
data: {
|
||||
title: 'Nested Page Chinese',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: 'cn/nested/meta.json',
|
||||
data: {
|
||||
title: 'Nested Chinese',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
test('Internationalized Routing: dir', () => {
|
||||
const result = loader({
|
||||
baseUrl: '/',
|
||||
i18n: {
|
||||
parser: 'dir',
|
||||
languages: ['cn', 'en'],
|
||||
defaultLanguage: 'en',
|
||||
},
|
||||
source: i18nSource2,
|
||||
});
|
||||
|
||||
expect(removeUndefined(result.pageTree['en'], true), 'Page Tree')
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"$id": "en",
|
||||
"children": [
|
||||
{
|
||||
"$id": "test.mdx",
|
||||
"$ref": {
|
||||
"file": "test.mdx",
|
||||
},
|
||||
"name": "Hello",
|
||||
"type": "page",
|
||||
"url": "/en/test",
|
||||
},
|
||||
{
|
||||
"$id": "nested",
|
||||
"children": [
|
||||
{
|
||||
"$id": "nested/test.mdx",
|
||||
"$ref": {
|
||||
"file": "nested/test.mdx",
|
||||
},
|
||||
"name": "Nested Page",
|
||||
"type": "page",
|
||||
"url": "/en/nested/test",
|
||||
},
|
||||
],
|
||||
"name": "Nested",
|
||||
"type": "folder",
|
||||
},
|
||||
],
|
||||
"name": "Docs English",
|
||||
}
|
||||
`);
|
||||
|
||||
expect(removeUndefined(result.pageTree['cn'], true), 'Page Tree')
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"$id": "cn",
|
||||
"children": [
|
||||
{
|
||||
"$id": "test.mdx",
|
||||
"$ref": {
|
||||
"file": "test.mdx",
|
||||
},
|
||||
"name": "Hello Chinese",
|
||||
"type": "page",
|
||||
"url": "/cn/test",
|
||||
},
|
||||
{
|
||||
"$id": "nested",
|
||||
"children": [
|
||||
{
|
||||
"$id": "nested/test.mdx",
|
||||
"$ref": {
|
||||
"file": "nested/test.mdx",
|
||||
},
|
||||
"name": "Nested Page Chinese",
|
||||
"type": "page",
|
||||
"url": "/cn/nested/test",
|
||||
},
|
||||
],
|
||||
"name": "Nested",
|
||||
"type": "folder",
|
||||
},
|
||||
],
|
||||
"name": "Docs Chinese",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user