Skip to content

Commit c002118

Browse files
authoredOct 21, 2024··
[v3] add tests for should respect order for type: "separator", type: "menu" and item with href (#3518)
* aa * aa * aa * aa * aa * aa * more * aa * aa * remove tests for /500 page * remove another test * aa * Update .changeset/few-cups-smoke.md
1 parent 8e9767e commit c002118

File tree

14 files changed

+252
-267
lines changed

14 files changed

+252
-267
lines changed
 

‎.changeset/few-cups-smoke.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'nextra-theme-docs': patch
3+
'nextra': patch
4+
---
5+
6+
- add tests for should respect order for `type: "separator"`, `type: "menu"` and item with `href`

‎.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ out/
1414

1515
tsup.config.bundled*
1616
tsconfig.tsbuildinfo
17-
pagefind/
17+
_pagefind/

‎packages/nextra-theme-docs/src/components/navbar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ function NavbarMenu({
5959
}
6060
>
6161
{children}
62+
<ArrowRightIcon className="_h-3.5 *:_origin-center *:_transition-transform *:_rotate-90" />
6263
</MenuButton>
6364
<MenuItems
6465
transition
@@ -136,7 +137,6 @@ export function Navbar({ items }: NavBarProps): ReactElement {
136137
return (
137138
<NavbarMenu key={menu.title} menu={menu}>
138139
{menu.title}
139-
<ArrowRightIcon className="_h-3.5 *:_origin-center *:_transition-transform *:_rotate-90" />
140140
</NavbarMenu>
141141
)
142142
}

‎packages/nextra/__test__/__snapshots__/normalize-page.spec.ts.snap

-88
Original file line numberDiff line numberDiff line change
@@ -81,94 +81,6 @@ exports[`normalize-page > /404 page 1`] = `
8181
}
8282
`;
8383

84-
exports[`normalize-page > /500 page 1`] = `
85-
{
86-
"activeIndex": 0,
87-
"activePath": [
88-
{
89-
"name": "500",
90-
"route": "/500",
91-
"title": "500",
92-
"type": "page",
93-
},
94-
],
95-
"activeThemeContext": {
96-
"breadcrumb": true,
97-
"collapsed": false,
98-
"footer": true,
99-
"layout": "raw",
100-
"navbar": true,
101-
"pagination": true,
102-
"sidebar": true,
103-
"timestamp": true,
104-
"toc": true,
105-
"typesetting": "default",
106-
},
107-
"activeType": "page",
108-
"directories": [
109-
{
110-
"name": "index",
111-
"route": "/",
112-
"title": "Introduction",
113-
"type": "doc",
114-
},
115-
{
116-
"name": "get-started",
117-
"route": "/get-started",
118-
"title": "Get Started",
119-
"type": "doc",
120-
},
121-
],
122-
"docsDirectories": [
123-
{
124-
"isUnderCurrentDocsTree": true,
125-
"name": "index",
126-
"route": "/",
127-
"title": "Introduction",
128-
"type": "doc",
129-
},
130-
{
131-
"isUnderCurrentDocsTree": true,
132-
"name": "get-started",
133-
"route": "/get-started",
134-
"title": "Get Started",
135-
"type": "doc",
136-
},
137-
],
138-
"flatDirectories": [
139-
{
140-
"name": "index",
141-
"route": "/",
142-
"title": "Introduction",
143-
"type": "doc",
144-
},
145-
{
146-
"name": "get-started",
147-
"route": "/get-started",
148-
"title": "Get Started",
149-
"type": "doc",
150-
},
151-
],
152-
"flatDocsDirectories": [
153-
{
154-
"isUnderCurrentDocsTree": true,
155-
"name": "index",
156-
"route": "/",
157-
"title": "Introduction",
158-
"type": "doc",
159-
},
160-
{
161-
"isUnderCurrentDocsTree": true,
162-
"name": "get-started",
163-
"route": "/get-started",
164-
"title": "Get Started",
165-
"type": "doc",
166-
},
167-
],
168-
"topLevelNavbarItems": [],
169-
}
170-
`;
171-
17284
exports[`normalize-page > en-US getting-started 1`] = `
17385
{
17486
"activeIndex": 1,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import one_two_meta from "./one/two/_meta.ts";
2+
export const pageMap = [{
3+
name: "one",
4+
route: "/one",
5+
children: [{
6+
name: "two",
7+
route: "/one/two",
8+
children: [{
9+
data: one_two_meta
10+
}, {
11+
name: "1-one",
12+
route: "/one/two/1-one",
13+
frontMatter: {
14+
"sidebarTitle": "1 One"
15+
}
16+
}, {
17+
name: "2024",
18+
route: "/one/two/2024",
19+
frontMatter: {
20+
"sidebarTitle": "2024"
21+
}
22+
}, {
23+
name: "foo",
24+
route: "/one/two/foo",
25+
frontMatter: {
26+
"sidebarTitle": "Foo"
27+
}
28+
}, {
29+
name: "one",
30+
route: "/one/two/one",
31+
frontMatter: {
32+
"sidebarTitle": "One"
33+
}
34+
}, {
35+
name: "qux",
36+
route: "/one/two/qux",
37+
frontMatter: {
38+
"sidebarTitle": "Qux"
39+
}
40+
}]
41+
}]
42+
}];

‎packages/nextra/__test__/fixture/page-maps/respect-order-for-type-separator-menu-and-item-with-href/one/two/1-one.md

Whitespace-only changes.

‎packages/nextra/__test__/fixture/page-maps/respect-order-for-type-separator-menu-and-item-with-href/one/two/2024.md

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export default {
2+
menu: {
3+
type: 'menu',
4+
items: {
5+
nextra: {
6+
title: 'Nextra',
7+
href: 'https://nextra.site'
8+
}
9+
}
10+
},
11+
'---': {
12+
type: 'separator'
13+
},
14+
qux: '',
15+
nextra: {
16+
title: 'Nextra',
17+
href: 'https://nextra.site'
18+
}
19+
}

‎packages/nextra/__test__/fixture/page-maps/respect-order-for-type-separator-menu-and-item-with-href/one/two/foo.md

Whitespace-only changes.

‎packages/nextra/__test__/fixture/page-maps/respect-order-for-type-separator-menu-and-item-with-href/one/two/one.md

Whitespace-only changes.

‎packages/nextra/__test__/fixture/page-maps/respect-order-for-type-separator-menu-and-item-with-href/one/two/qux.md

Whitespace-only changes.

‎packages/nextra/__test__/normalize-page.spec.ts

+122-110
Original file line numberDiff line numberDiff line change
@@ -48,116 +48,6 @@ describe('normalize-page', () => {
4848
expect(result).toMatchSnapshot()
4949
})
5050

51-
it('/500 page', () => {
52-
const result = normalizePages({
53-
list: [
54-
{ name: '500', route: '/500' },
55-
{ name: 'get-started', route: '/get-started' },
56-
{ name: 'index', route: '/' },
57-
{
58-
data: {
59-
'500': {
60-
type: 'page',
61-
theme: {
62-
layout: 'raw'
63-
}
64-
},
65-
index: 'Introduction',
66-
'get-started': 'Get Started'
67-
}
68-
}
69-
],
70-
route: '/500'
71-
})
72-
expect(result).toMatchSnapshot()
73-
})
74-
75-
// https://github.com/shuding/nextra/issues/1888
76-
it('should set `route: #` for `type: menu`', () => {
77-
const result = normalizePages({
78-
list: [
79-
{
80-
data: {
81-
index: {
82-
type: 'page',
83-
title: 'Nextra',
84-
display: 'hidden'
85-
},
86-
docs: {
87-
type: 'page',
88-
title: 'Documentation'
89-
},
90-
explorers: {
91-
title: 'Explorers',
92-
type: 'menu'
93-
},
94-
showcase: {
95-
type: 'page',
96-
title: 'Showcase'
97-
},
98-
explorers2: {
99-
title: 'Explorers2',
100-
type: 'menu'
101-
},
102-
about: {
103-
type: 'page',
104-
title: 'About'
105-
},
106-
explorers3: {
107-
title: 'Explorers3',
108-
type: 'menu'
109-
}
110-
}
111-
},
112-
{
113-
name: 'about',
114-
route: '/about'
115-
},
116-
{
117-
name: 'showcase',
118-
route: '/showcase'
119-
}
120-
],
121-
route: '/docs'
122-
})
123-
expect(result.topLevelNavbarItems).toMatchInlineSnapshot(`
124-
[
125-
{
126-
"name": "docs",
127-
"title": "Documentation",
128-
"type": "page",
129-
},
130-
{
131-
"name": "explorers",
132-
"title": "Explorers",
133-
"type": "menu",
134-
},
135-
{
136-
"name": "showcase",
137-
"route": "/showcase",
138-
"title": "Showcase",
139-
"type": "page",
140-
},
141-
{
142-
"name": "explorers2",
143-
"title": "Explorers2",
144-
"type": "menu",
145-
},
146-
{
147-
"name": "about",
148-
"route": "/about",
149-
"title": "About",
150-
"type": "page",
151-
},
152-
{
153-
"name": "explorers3",
154-
"title": "Explorers3",
155-
"type": "menu",
156-
},
157-
]
158-
`)
159-
})
160-
16151
// https://github.com/shuding/nextra/issues/3331
16252
it('should keep `activeThemeContext`, `activeType` for hidden route', async () => {
16353
const dir = path.join(
@@ -324,4 +214,126 @@ describe('normalize-page', () => {
324214
}
325215
})
326216
})
217+
218+
it('should respect order for `type: "separator"`, `type: "menu"` and item with `href`', async () => {
219+
const dir = path.join(
220+
__dirname,
221+
'fixture',
222+
'page-maps',
223+
'respect-order-for-type-separator-menu-and-item-with-href'
224+
)
225+
vi.doMock('../src/server/file-system.ts', () => ({ PAGES_DIR: dir }))
226+
vi.doMock('../src/server/constants.ts', async () => ({
227+
...(await vi.importActual('../src/server/constants.ts')),
228+
CHUNKS_DIR: dir
229+
}))
230+
const { collectPageMap } = await import('../src/server/page-map.js')
231+
232+
const result = await collectPageMap({ dir })
233+
await fs.writeFile(path.join(dir, 'generated-page-map.ts'), result)
234+
235+
const { pageMap } = await import(
236+
'./fixture/page-maps/respect-order-for-type-separator-menu-and-item-with-href/generated-page-map.js'
237+
)
238+
239+
const normalizedResult = normalizePages({
240+
list: pageMap,
241+
route: '/one/two/qux'
242+
})
243+
expect(normalizedResult.docsDirectories).toMatchInlineSnapshot(`
244+
[
245+
{
246+
"children": [
247+
{
248+
"children": [
249+
{
250+
"items": {
251+
"nextra": {
252+
"href": "https://nextra.site",
253+
"title": "Nextra",
254+
},
255+
},
256+
"name": "menu",
257+
"title": "menu",
258+
"type": "menu",
259+
},
260+
{
261+
"isUnderCurrentDocsTree": true,
262+
"name": "---",
263+
"type": "separator",
264+
},
265+
{
266+
"frontMatter": {
267+
"sidebarTitle": "Qux",
268+
},
269+
"isUnderCurrentDocsTree": true,
270+
"name": "qux",
271+
"route": "/one/two/qux",
272+
"title": "Qux",
273+
"type": "doc",
274+
},
275+
{
276+
"href": "https://nextra.site",
277+
"isUnderCurrentDocsTree": true,
278+
"name": "nextra",
279+
"title": "Nextra",
280+
"type": "doc",
281+
},
282+
{
283+
"frontMatter": {
284+
"sidebarTitle": "1 One",
285+
},
286+
"isUnderCurrentDocsTree": true,
287+
"name": "1-one",
288+
"route": "/one/two/1-one",
289+
"title": "1 One",
290+
"type": "doc",
291+
},
292+
{
293+
"frontMatter": {
294+
"sidebarTitle": "2024",
295+
},
296+
"isUnderCurrentDocsTree": true,
297+
"name": "2024",
298+
"route": "/one/two/2024",
299+
"title": "2024",
300+
"type": "doc",
301+
},
302+
{
303+
"frontMatter": {
304+
"sidebarTitle": "Foo",
305+
},
306+
"isUnderCurrentDocsTree": true,
307+
"name": "foo",
308+
"route": "/one/two/foo",
309+
"title": "Foo",
310+
"type": "doc",
311+
},
312+
{
313+
"frontMatter": {
314+
"sidebarTitle": "One",
315+
},
316+
"isUnderCurrentDocsTree": true,
317+
"name": "one",
318+
"route": "/one/two/one",
319+
"title": "One",
320+
"type": "doc",
321+
},
322+
],
323+
"isUnderCurrentDocsTree": true,
324+
"name": "two",
325+
"route": "/one/two",
326+
"title": "two",
327+
"type": "doc",
328+
},
329+
],
330+
"isUnderCurrentDocsTree": true,
331+
"name": "one",
332+
"route": "/one",
333+
"title": "one",
334+
"type": "doc",
335+
},
336+
]
337+
`)
338+
})
327339
})

‎packages/nextra/src/client/normalize-pages.ts

+60-66
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
menuItemSchema,
66
pageThemeSchema
77
} from '../server/schemas'
8-
import type { Folder, MdxFile, PageMapItem } from '../types'
8+
import type { Folder, MdxFile, MetaJsonFile, PageMapItem } from '../types'
99

1010
const DEFAULT_PAGE_THEME: PageTheme = {
1111
breadcrumb: true,
@@ -24,11 +24,12 @@ export type PageTheme = z.infer<typeof pageThemeSchema>
2424

2525
type Display = z.infer<typeof displaySchema>
2626
type IMenuItem = z.infer<typeof menuItemSchema>
27+
type MetaType = Record<string, any>
2728

2829
function extendMeta(
29-
_meta: Record<string, any> = {},
30-
fallback: Record<string, any>,
31-
metadata: Record<string, any> = {}
30+
_meta: MetaType = {},
31+
fallback: MetaType,
32+
metadata: MetaType = {}
3233
): Record<string, any> {
3334
const theme: PageTheme = {
3435
...fallback.theme,
@@ -116,74 +117,44 @@ export function normalizePages({
116117
underCurrentDocsRoot?: boolean
117118
pageThemeContext?: PageTheme
118119
}): NormalizedResult {
119-
let _meta: Record<string, any> | undefined
120-
for (const item of list) {
120+
let meta: MetaType = {}
121+
let metaKeys: (keyof MetaType)[] = []
122+
const items: any[] = []
123+
124+
for (const [index, item] of list.entries()) {
121125
if ('data' in item) {
122-
_meta = item.data
123-
break
126+
meta = item.data
127+
metaKeys = Object.keys(meta).filter(key => key !== '*')
128+
for (const key of metaKeys) {
129+
if (typeof meta[key] !== 'string') continue
130+
meta[key] = { title: meta[key] }
131+
}
132+
continue
124133
}
125-
}
126-
const meta = _meta || {}
127-
const metaKeys = Object.keys(meta).filter(key => key !== '*')
128-
129-
for (const key of metaKeys) {
130-
if (typeof meta[key] === 'string') {
131-
meta[key] = {
132-
title: meta[key]
134+
const prevItem = list[index - 1] as Exclude<PageMapItem, MetaJsonFile>
135+
136+
// If there are two items with the same name, they must be a directory and a
137+
// page. In that case we merge them, and use the page's link.
138+
if (prevItem && prevItem.name === item.name) {
139+
items[items.length - 1] = {
140+
...prevItem,
141+
withIndexPage: true,
142+
// @ts-expect-error fixme
143+
frontMatter: item.frontMatter
133144
}
145+
continue
134146
}
147+
items.push(item)
135148
}
136-
137-
// All directories
138-
// - directories: all directories in the tree structure
139-
// - flatDirectories: all directories in the flat structure, used by search and footer navigation
140-
const directories: Item[] = []
141-
const flatDirectories: Item[] = []
142-
143-
// Docs directories
144-
const docsDirectories: DocsItem[] = []
145-
const flatDocsDirectories: DocsItem[] = []
146-
147-
// Page directories
148-
const topLevelNavbarItems: (PageItem | MenuItem)[] = []
149-
150-
const { title: _title, href: _href, ...fallbackMeta } = meta['*'] || {}
151-
152-
let activeType: string = fallbackMeta.type
153-
let activeIndex = 0
154-
let activeThemeContext = {
155-
...pageThemeContext,
156-
...fallbackMeta.theme
157-
}
158-
let activePath: Item[] = []
159-
160149
// Normalize items based on files and _meta.json.
161-
const items: any[] = list
162-
.filter((a): a is MdxFile | Folder => !('data' in a))
163-
.sort((a, b) => {
164-
const indexA = metaKeys.indexOf(a.name)
165-
const indexB = metaKeys.indexOf(b.name)
166-
if (indexA === -1 && indexB === -1) return a.name < b.name ? -1 : 1
167-
if (indexA === -1) return 1
168-
if (indexB === -1) return -1
169-
return indexA - indexB
170-
})
171-
.flatMap((currentItem, index, list) => {
172-
const nextItem = list[index + 1]
173-
// If there are two items with the same name, they must be a directory and a
174-
// page. In that case we merge them, and use the page's link.
175-
if (nextItem && nextItem.name === currentItem.name) {
176-
list[index + 1] = {
177-
...currentItem,
178-
// @ts-expect-error fixme
179-
withIndexPage: true,
180-
// @ts-expect-error fixme
181-
frontMatter: nextItem.frontMatter
182-
}
183-
return []
184-
}
185-
return [currentItem]
186-
})
150+
items.sort((a, b) => {
151+
const indexB = metaKeys.indexOf(b.name)
152+
const indexA = metaKeys.indexOf(a.name)
153+
if (indexB === -1) {
154+
return indexA === -1 ? a.name - b.name : -1
155+
}
156+
return indexA - indexB
157+
})
187158

188159
for (const [index, metaKey] of metaKeys.entries()) {
189160
if (items.some(item => item.name === metaKey)) continue
@@ -213,6 +184,29 @@ The field key "${metaKey}" in \`_meta\` file refers to a page that cannot be fou
213184
)
214185
}
215186

187+
// All directories
188+
// - directories: all directories in the tree structure
189+
// - flatDirectories: all directories in the flat structure, used by search and footer navigation
190+
const directories: Item[] = []
191+
const flatDirectories: Item[] = []
192+
193+
// Docs directories
194+
const docsDirectories: DocsItem[] = []
195+
const flatDocsDirectories: DocsItem[] = []
196+
197+
// Page directories
198+
const topLevelNavbarItems: (PageItem | MenuItem)[] = []
199+
200+
const { title: _title, href: _href, ...fallbackMeta } = meta['*'] || {}
201+
202+
let activeType: string = fallbackMeta.type
203+
let activeIndex = 0
204+
let activeThemeContext = {
205+
...pageThemeContext,
206+
...fallbackMeta.theme
207+
}
208+
let activePath: Item[] = []
209+
216210
for (const currentItem of items) {
217211
// Get the item's meta information.
218212
const extendedMeta = extendMeta(

‎packages/nextra/src/server/page-map.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function cleanFileName(name: string): string {
4141
return (
4242
path
4343
.relative(PAGES_DIR, name)
44-
.replace(/\.([jt]sx?|json|mdx?)$/, '')
44+
.replace(/\.([jt]sx?|mdx?)$/, '')
4545
.replaceAll(/[\W_]+/g, '_')
4646
.replace(/^_/, '')
4747
// Variable can't start with number

0 commit comments

Comments
 (0)
Please sign in to comment.