@docusaurus/plugin-content-docs 0.0.0-4238 → 0.0.0-4243

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/lib/.tsbuildinfo +1 -1
  2. package/lib/categoryGeneratedIndex.d.ts +12 -0
  3. package/lib/categoryGeneratedIndex.js +37 -0
  4. package/lib/cli.js +5 -23
  5. package/lib/docs.d.ts +22 -2
  6. package/lib/docs.js +71 -29
  7. package/lib/index.js +34 -62
  8. package/lib/options.js +2 -0
  9. package/lib/props.js +35 -6
  10. package/lib/routes.d.ts +27 -0
  11. package/lib/routes.js +105 -0
  12. package/lib/sidebars/generator.d.ts +2 -1
  13. package/lib/sidebars/generator.js +55 -13
  14. package/lib/sidebars/index.d.ts +5 -4
  15. package/lib/sidebars/index.js +18 -9
  16. package/lib/sidebars/normalization.d.ts +8 -3
  17. package/lib/sidebars/normalization.js +36 -17
  18. package/lib/sidebars/processor.d.ts +5 -3
  19. package/lib/sidebars/processor.js +33 -18
  20. package/lib/sidebars/types.d.ts +43 -2
  21. package/lib/sidebars/utils.d.ts +18 -6
  22. package/lib/sidebars/utils.js +149 -24
  23. package/lib/sidebars/validation.d.ts +2 -0
  24. package/lib/sidebars/validation.js +44 -8
  25. package/lib/slug.d.ts +4 -3
  26. package/lib/slug.js +26 -14
  27. package/lib/translations.js +51 -7
  28. package/lib/types.d.ts +18 -3
  29. package/package.json +8 -8
  30. package/src/__tests__/__fixtures__/versioned-site/versioned_sidebars/version-1.0.1-sidebars.json +2 -2
  31. package/src/__tests__/__snapshots__/cli.test.ts.snap +48 -106
  32. package/src/__tests__/__snapshots__/index.test.ts.snap +279 -28
  33. package/src/__tests__/__snapshots__/translations.test.ts.snap +45 -0
  34. package/src/__tests__/docs.test.ts +122 -7
  35. package/src/__tests__/index.test.ts +27 -1
  36. package/src/__tests__/options.test.ts +2 -0
  37. package/src/__tests__/slug.test.ts +127 -20
  38. package/src/__tests__/translations.test.ts +7 -0
  39. package/src/categoryGeneratedIndex.ts +57 -0
  40. package/src/cli.ts +5 -35
  41. package/src/docs.ts +103 -45
  42. package/src/index.ts +55 -93
  43. package/src/options.ts +4 -0
  44. package/src/plugin-content-docs.d.ts +71 -8
  45. package/src/props.ts +48 -9
  46. package/src/routes.ts +173 -0
  47. package/src/sidebars/__tests__/__snapshots__/index.test.ts.snap +21 -6
  48. package/src/sidebars/__tests__/generator.test.ts +105 -1
  49. package/src/sidebars/__tests__/index.test.ts +26 -24
  50. package/src/sidebars/__tests__/processor.test.ts +110 -19
  51. package/src/sidebars/__tests__/utils.test.ts +320 -20
  52. package/src/sidebars/__tests__/validation.test.ts +105 -0
  53. package/src/sidebars/generator.ts +82 -19
  54. package/src/sidebars/index.ts +23 -13
  55. package/src/sidebars/normalization.ts +47 -23
  56. package/src/sidebars/processor.ts +57 -27
  57. package/src/sidebars/types.ts +64 -3
  58. package/src/sidebars/utils.ts +217 -42
  59. package/src/sidebars/validation.ts +52 -8
  60. package/src/slug.ts +32 -17
  61. package/src/translations.ts +74 -8
  62. package/src/types.ts +22 -5
@@ -12,8 +12,12 @@ import {
12
12
  collectSidebarLinks,
13
13
  transformSidebarItems,
14
14
  collectSidebarsDocIds,
15
+ SidebarNavigation,
16
+ toDocNavigationLink,
17
+ toNavigationLink,
15
18
  } from '../utils';
16
19
  import type {Sidebar, Sidebars} from '../types';
20
+ import {DocMetadataBase, DocNavLink} from '../../types';
17
21
 
18
22
  describe('createSidebarsUtils', () => {
19
23
  const sidebar1: Sidebar = [
@@ -21,13 +25,13 @@ describe('createSidebarsUtils', () => {
21
25
  type: 'category',
22
26
  collapsed: false,
23
27
  collapsible: true,
24
- label: 'Category1',
28
+ label: 'S1 Category',
25
29
  items: [
26
30
  {
27
31
  type: 'category',
28
32
  collapsed: false,
29
33
  collapsible: true,
30
- label: 'Subcategory 1',
34
+ label: 'S1 Subcategory',
31
35
  items: [{type: 'doc', id: 'doc1'}],
32
36
  },
33
37
  {type: 'doc', id: 'doc2'},
@@ -40,7 +44,7 @@ describe('createSidebarsUtils', () => {
40
44
  type: 'category',
41
45
  collapsed: false,
42
46
  collapsible: true,
43
- label: 'Category2',
47
+ label: 'S2 Category',
44
48
  items: [
45
49
  {type: 'doc', id: 'doc3'},
46
50
  {type: 'doc', id: 'doc4'},
@@ -48,10 +52,58 @@ describe('createSidebarsUtils', () => {
48
52
  },
49
53
  ];
50
54
 
51
- const sidebars: Sidebars = {sidebar1, sidebar2};
55
+ const sidebar3: Sidebar = [
56
+ {
57
+ type: 'category',
58
+ collapsed: false,
59
+ collapsible: true,
60
+ label: 'S3 Category',
61
+ link: {
62
+ type: 'doc',
63
+ id: 'doc5',
64
+ },
65
+ items: [
66
+ {
67
+ type: 'category',
68
+ collapsed: false,
69
+ collapsible: true,
70
+ label: 'S3 SubCategory',
71
+ link: {
72
+ type: 'generated-index',
73
+ slug: '/s3-subcategory-index-slug',
74
+ permalink: '/s3-subcategory-index-permalink',
75
+ },
76
+ items: [
77
+ {
78
+ type: 'category',
79
+ collapsed: false,
80
+ collapsible: true,
81
+ label: 'S3 SubSubCategory',
82
+ link: {
83
+ type: 'generated-index',
84
+ slug: '/s3-subsubcategory-slug',
85
+ permalink: '/s3-subsubcategory-index-permalink',
86
+ },
87
+ items: [
88
+ {type: 'doc', id: 'doc6'},
89
+ {type: 'doc', id: 'doc7'},
90
+ ],
91
+ },
92
+ ],
93
+ },
94
+ ],
95
+ },
96
+ ];
97
+
98
+ const sidebars: Sidebars = {sidebar1, sidebar2, sidebar3};
52
99
 
53
- const {getFirstDocIdOfFirstSidebar, getSidebarNameByDocId, getDocNavigation} =
54
- createSidebarsUtils(sidebars);
100
+ const {
101
+ getFirstDocIdOfFirstSidebar,
102
+ getSidebarNameByDocId,
103
+ getDocNavigation,
104
+ getCategoryGeneratedIndexNavigation,
105
+ getCategoryGeneratedIndexList,
106
+ } = createSidebarsUtils(sidebars);
55
107
 
56
108
  test('getSidebarNameByDocId', async () => {
57
109
  expect(getFirstDocIdOfFirstSidebar()).toEqual('doc1');
@@ -62,32 +114,117 @@ describe('createSidebarsUtils', () => {
62
114
  expect(getSidebarNameByDocId('doc2')).toEqual('sidebar1');
63
115
  expect(getSidebarNameByDocId('doc3')).toEqual('sidebar2');
64
116
  expect(getSidebarNameByDocId('doc4')).toEqual('sidebar2');
65
- expect(getSidebarNameByDocId('doc5')).toEqual(undefined);
66
- expect(getSidebarNameByDocId('doc6')).toEqual(undefined);
117
+ expect(getSidebarNameByDocId('doc5')).toEqual('sidebar3');
118
+ expect(getSidebarNameByDocId('doc6')).toEqual('sidebar3');
119
+ expect(getSidebarNameByDocId('doc7')).toEqual('sidebar3');
120
+ expect(getSidebarNameByDocId('unknown_id')).toEqual(undefined);
67
121
  });
68
122
 
69
123
  test('getDocNavigation', async () => {
70
124
  expect(getDocNavigation('doc1')).toEqual({
71
125
  sidebarName: 'sidebar1',
72
- previousId: undefined,
73
- nextId: 'doc2',
74
- });
126
+ previous: undefined,
127
+ next: {
128
+ type: 'doc',
129
+ id: 'doc2',
130
+ },
131
+ } as SidebarNavigation);
75
132
  expect(getDocNavigation('doc2')).toEqual({
76
133
  sidebarName: 'sidebar1',
77
- previousId: 'doc1',
78
- nextId: undefined,
79
- });
134
+ previous: {
135
+ type: 'doc',
136
+ id: 'doc1',
137
+ },
138
+ next: undefined,
139
+ } as SidebarNavigation);
80
140
 
81
141
  expect(getDocNavigation('doc3')).toEqual({
82
142
  sidebarName: 'sidebar2',
83
- previousId: undefined,
84
- nextId: 'doc4',
85
- });
143
+ previous: undefined,
144
+ next: {
145
+ type: 'doc',
146
+ id: 'doc4',
147
+ },
148
+ } as SidebarNavigation);
86
149
  expect(getDocNavigation('doc4')).toEqual({
87
150
  sidebarName: 'sidebar2',
88
- previousId: 'doc3',
89
- nextId: undefined,
90
- });
151
+ previous: {
152
+ type: 'doc',
153
+ id: 'doc3',
154
+ },
155
+ next: undefined,
156
+ } as SidebarNavigation);
157
+
158
+ expect(getDocNavigation('doc5')).toMatchObject({
159
+ sidebarName: 'sidebar3',
160
+ previous: undefined,
161
+ next: {
162
+ type: 'category',
163
+ label: 'S3 SubCategory',
164
+ },
165
+ } as SidebarNavigation);
166
+ expect(getDocNavigation('doc6')).toMatchObject({
167
+ sidebarName: 'sidebar3',
168
+ previous: {
169
+ type: 'category',
170
+ label: 'S3 SubSubCategory',
171
+ },
172
+ next: {
173
+ type: 'doc',
174
+ id: 'doc7',
175
+ },
176
+ } as SidebarNavigation);
177
+ expect(getDocNavigation('doc7')).toMatchObject({
178
+ sidebarName: 'sidebar3',
179
+ previous: {
180
+ type: 'doc',
181
+ id: 'doc6',
182
+ },
183
+ next: undefined,
184
+ } as SidebarNavigation);
185
+ });
186
+
187
+ test('getCategoryGeneratedIndexNavigation', async () => {
188
+ expect(
189
+ getCategoryGeneratedIndexNavigation('/s3-subcategory-index-permalink'),
190
+ ).toMatchObject({
191
+ sidebarName: 'sidebar3',
192
+ previous: {
193
+ type: 'category',
194
+ label: 'S3 Category',
195
+ },
196
+ next: {
197
+ type: 'category',
198
+ label: 'S3 SubSubCategory',
199
+ },
200
+ } as SidebarNavigation);
201
+
202
+ expect(
203
+ getCategoryGeneratedIndexNavigation('/s3-subsubcategory-index-permalink'),
204
+ ).toMatchObject({
205
+ sidebarName: 'sidebar3',
206
+ previous: {
207
+ type: 'category',
208
+ label: 'S3 SubCategory',
209
+ },
210
+ next: {
211
+ type: 'doc',
212
+ id: 'doc6',
213
+ },
214
+ } as SidebarNavigation);
215
+ });
216
+
217
+ test('getCategoryGeneratedIndexList', async () => {
218
+ expect(getCategoryGeneratedIndexList()).toMatchObject([
219
+ {
220
+ type: 'category',
221
+ label: 'S3 SubCategory',
222
+ },
223
+ {
224
+ type: 'category',
225
+ label: 'S3 SubSubCategory',
226
+ },
227
+ ]);
91
228
  });
92
229
  });
93
230
 
@@ -393,3 +530,166 @@ describe('transformSidebarItems', () => {
393
530
  ]);
394
531
  });
395
532
  });
533
+
534
+ describe('toDocNavigationLink', () => {
535
+ type TestDoc = Pick<DocMetadataBase, 'permalink' | 'title' | 'frontMatter'>;
536
+ function testDoc(data: TestDoc) {
537
+ return data as DocMetadataBase;
538
+ }
539
+
540
+ test('with no frontmatter', () => {
541
+ expect(
542
+ toDocNavigationLink(
543
+ testDoc({
544
+ title: 'Doc Title',
545
+ permalink: '/docPermalink',
546
+ frontMatter: {},
547
+ }),
548
+ ),
549
+ ).toEqual({
550
+ title: 'Doc Title',
551
+ permalink: '/docPermalink',
552
+ } as DocNavLink);
553
+ });
554
+
555
+ test('with pagination_label frontmatter', () => {
556
+ expect(
557
+ toDocNavigationLink(
558
+ testDoc({
559
+ title: 'Doc Title',
560
+ permalink: '/docPermalink',
561
+ frontMatter: {
562
+ pagination_label: 'pagination_label',
563
+ },
564
+ }),
565
+ ),
566
+ ).toEqual({
567
+ title: 'pagination_label',
568
+ permalink: '/docPermalink',
569
+ } as DocNavLink);
570
+ });
571
+
572
+ test('with sidebar_label frontmatter', () => {
573
+ expect(
574
+ toDocNavigationLink(
575
+ testDoc({
576
+ title: 'Doc Title',
577
+ permalink: '/docPermalink',
578
+ frontMatter: {
579
+ sidebar_label: 'sidebar_label',
580
+ },
581
+ }),
582
+ ),
583
+ ).toEqual({
584
+ title: 'sidebar_label',
585
+ permalink: '/docPermalink',
586
+ } as DocNavLink);
587
+ });
588
+
589
+ test('with pagination_label + sidebar_label frontmatter', () => {
590
+ expect(
591
+ toDocNavigationLink(
592
+ testDoc({
593
+ title: 'Doc Title',
594
+ permalink: '/docPermalink',
595
+ frontMatter: {
596
+ pagination_label: 'pagination_label',
597
+ sidebar_label: 'sidebar_label',
598
+ },
599
+ }),
600
+ ),
601
+ ).toEqual({
602
+ title: 'pagination_label',
603
+ permalink: '/docPermalink',
604
+ } as DocNavLink);
605
+ });
606
+ });
607
+
608
+ describe('toNavigationLink', () => {
609
+ type TestDoc = Pick<DocMetadataBase, 'permalink' | 'title'>;
610
+ function testDoc(data: TestDoc) {
611
+ return {...data, frontMatter: {}} as DocMetadataBase;
612
+ }
613
+
614
+ const docsById: Record<string, DocMetadataBase> = {
615
+ doc1: testDoc({
616
+ title: 'Doc 1',
617
+ permalink: '/doc1',
618
+ }),
619
+ doc2: testDoc({
620
+ title: 'Doc 1',
621
+ permalink: '/doc1',
622
+ }),
623
+ };
624
+
625
+ test('with doc items', () => {
626
+ expect(toNavigationLink({type: 'doc', id: 'doc1'}, docsById)).toEqual(
627
+ toDocNavigationLink(docsById.doc1),
628
+ );
629
+ expect(toNavigationLink({type: 'doc', id: 'doc2'}, docsById)).toEqual(
630
+ toDocNavigationLink(docsById.doc2),
631
+ );
632
+ expect(() =>
633
+ toNavigationLink({type: 'doc', id: 'doc3'}, docsById),
634
+ ).toThrowErrorMatchingInlineSnapshot(
635
+ `"Can't create navigation link: no doc found with id=doc3"`,
636
+ );
637
+ });
638
+
639
+ test('with category item and doc link', () => {
640
+ expect(
641
+ toNavigationLink(
642
+ {
643
+ type: 'category',
644
+ label: 'Category',
645
+ items: [],
646
+ link: {
647
+ type: 'doc',
648
+ id: 'doc1',
649
+ },
650
+ collapsed: true,
651
+ collapsible: true,
652
+ },
653
+ docsById,
654
+ ),
655
+ ).toEqual(toDocNavigationLink(docsById.doc1));
656
+ expect(() =>
657
+ toNavigationLink(
658
+ {
659
+ type: 'category',
660
+ label: 'Category',
661
+ items: [],
662
+ link: {
663
+ type: 'doc',
664
+ id: 'doc3',
665
+ },
666
+ collapsed: true,
667
+ collapsible: true,
668
+ },
669
+ docsById,
670
+ ),
671
+ ).toThrowErrorMatchingInlineSnapshot(
672
+ `"Can't create navigation link: no doc found with id=doc3"`,
673
+ );
674
+ });
675
+
676
+ test('with category item and generated-index link', () => {
677
+ expect(
678
+ toNavigationLink(
679
+ {
680
+ type: 'category',
681
+ label: 'Category',
682
+ items: [],
683
+ link: {
684
+ type: 'generated-index',
685
+ slug: 'slug',
686
+ permalink: 'generated-index-permalink',
687
+ },
688
+ collapsed: true,
689
+ collapsible: true,
690
+ },
691
+ docsById,
692
+ ),
693
+ ).toEqual({title: 'Category', permalink: 'generated-index-permalink'});
694
+ });
695
+ });
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import {validateSidebars, validateCategoryMetadataFile} from '../validation';
9
+ import {CategoryMetadataFile} from '../generator';
10
+ import {SidebarsConfig} from '../types';
11
+
12
+ describe('validateSidebars', () => {
13
+ // TODO add more tests
14
+
15
+ // TODO it seems many error cases are not validated properly
16
+ // and error messages are quite bad
17
+ test('throw for bad value', async () => {
18
+ expect(() => validateSidebars({sidebar: [{type: 42}]}))
19
+ .toThrowErrorMatchingInlineSnapshot(`
20
+ "{
21
+ \\"type\\": 42,
22
+ \\"undefined\\" [1]: -- missing --
23
+ }
24
+ 
25
+ [1] Unknown sidebar item type \\"42\\"."
26
+ `);
27
+ });
28
+
29
+ test('accept empty object', async () => {
30
+ const sidebars: SidebarsConfig = {};
31
+ validateSidebars(sidebars);
32
+ });
33
+
34
+ test('accept valid values', async () => {
35
+ const sidebars: SidebarsConfig = {
36
+ sidebar1: [
37
+ {type: 'doc', id: 'doc1'},
38
+ {type: 'doc', id: 'doc2'},
39
+ {
40
+ type: 'category',
41
+ label: 'Category',
42
+ items: [{type: 'doc', id: 'doc3'}],
43
+ },
44
+ ],
45
+ };
46
+ validateSidebars(sidebars);
47
+ });
48
+ });
49
+
50
+ describe('validateCategoryMetadataFile', () => {
51
+ // TODO add more tests
52
+
53
+ test('throw for bad value', async () => {
54
+ expect(() =>
55
+ validateCategoryMetadataFile(42),
56
+ ).toThrowErrorMatchingInlineSnapshot(
57
+ `"\\"value\\" must be of type object"`,
58
+ );
59
+ });
60
+
61
+ test('accept empty object', async () => {
62
+ const content: CategoryMetadataFile = {};
63
+ expect(validateCategoryMetadataFile(content)).toEqual(content);
64
+ });
65
+
66
+ test('accept valid values', async () => {
67
+ const content: CategoryMetadataFile = {
68
+ className: 'className',
69
+ label: 'Category Label',
70
+ link: {
71
+ type: 'generated-index',
72
+ slug: 'slug',
73
+ title: 'title',
74
+ description: 'description',
75
+ },
76
+ collapsible: true,
77
+ collapsed: true,
78
+ position: 3,
79
+ };
80
+ expect(validateCategoryMetadataFile(content)).toEqual(content);
81
+ });
82
+
83
+ test('rejects permalink', async () => {
84
+ const content: CategoryMetadataFile = {
85
+ className: 'className',
86
+ label: 'Category Label',
87
+ link: {
88
+ type: 'generated-index',
89
+ slug: 'slug',
90
+ // @ts-expect-error: rejected on purpose
91
+ permalink: 'somePermalink',
92
+ title: 'title',
93
+ description: 'description',
94
+ },
95
+ collapsible: true,
96
+ collapsed: true,
97
+ position: 3,
98
+ };
99
+ expect(() =>
100
+ validateCategoryMetadataFile(content),
101
+ ).toThrowErrorMatchingInlineSnapshot(
102
+ `"\\"link.permalink\\" is not allowed"`,
103
+ );
104
+ });
105
+ });
@@ -11,19 +11,27 @@ import type {
11
11
  SidebarItemCategory,
12
12
  SidebarItemsGenerator,
13
13
  SidebarItemsGeneratorDoc,
14
+ SidebarItemCategoryLink,
15
+ SidebarItemCategoryLinkConfig,
14
16
  } from './types';
15
- import {keyBy, sortBy} from 'lodash';
17
+ import {sortBy, last} from 'lodash';
16
18
  import {addTrailingSlash, posixPath} from '@docusaurus/utils';
17
- import {Joi} from '@docusaurus/utils-validation';
18
19
  import chalk from 'chalk';
19
20
  import path from 'path';
20
21
  import fs from 'fs-extra';
21
22
  import Yaml from 'js-yaml';
23
+ import {validateCategoryMetadataFile} from './validation';
24
+ import {createDocsByIdIndex, isConventionalDocIndex} from '../docs';
22
25
 
23
26
  const BreadcrumbSeparator = '/';
24
27
  // To avoid possible name clashes with a folder of the same name as the ID
25
28
  const docIdPrefix = '$doc$/';
26
29
 
30
+ // Just an alias to the make code more explicit
31
+ function getLocalDocId(docId: string): string {
32
+ return last(docId.split('/'))!;
33
+ }
34
+
27
35
  export const CategoryMetadataFilenameBase = '_category_';
28
36
  export const CategoryMetadataFilenamePattern = '_category_.{json,yml,yaml}';
29
37
 
@@ -33,6 +41,7 @@ export type CategoryMetadataFile = {
33
41
  collapsed?: boolean;
34
42
  collapsible?: boolean;
35
43
  className?: string;
44
+ link?: SidebarItemCategoryLinkConfig;
36
45
 
37
46
  // TODO should we allow "items" here? how would this work? would an "autogenerated" type be allowed?
38
47
  // This mkdocs plugin do something like that: https://github.com/lukasgeiter/mkdocs-awesome-pages-plugin/
@@ -50,17 +59,9 @@ type Dir = {
50
59
  [item: string]: Dir | null;
51
60
  };
52
61
 
53
- const CategoryMetadataFileSchema = Joi.object<CategoryMetadataFile>({
54
- label: Joi.string(),
55
- position: Joi.number(),
56
- collapsed: Joi.boolean(),
57
- collapsible: Joi.boolean(),
58
- className: Joi.string(),
59
- });
60
-
61
62
  // TODO I now believe we should read all the category metadata files ahead of time: we may need this metadata to customize docs metadata
62
63
  // Example use-case being able to disable number prefix parsing at the folder level, or customize the default route path segment for an intermediate directory...
63
- // TODO later if there is `CategoryFolder/index.md`, we may want to read the metadata as yaml on it
64
+ // TODO later if there is `CategoryFolder/with-category-name-doc.md`, we may want to read the metadata as yaml on it
64
65
  // see https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
65
66
  async function readCategoryMetadataFile(
66
67
  categoryDirPath: string,
@@ -69,7 +70,7 @@ async function readCategoryMetadataFile(
69
70
  const contentString = await fs.readFile(filePath, {encoding: 'utf8'});
70
71
  const unsafeContent = Yaml.load(contentString);
71
72
  try {
72
- return Joi.attempt(unsafeContent, CategoryMetadataFileSchema);
73
+ return validateCategoryMetadataFile(unsafeContent);
73
74
  } catch (e) {
74
75
  console.error(
75
76
  chalk.red(
@@ -100,6 +101,21 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
100
101
  item: {dirName: autogenDir},
101
102
  version,
102
103
  }) => {
104
+ const docsById = createDocsByIdIndex(allDocs);
105
+ const findDoc = (docId: string): SidebarItemsGeneratorDoc | undefined =>
106
+ docsById[docId];
107
+ const getDoc = (docId: string): SidebarItemsGeneratorDoc => {
108
+ const doc = findDoc(docId);
109
+ if (!doc) {
110
+ throw new Error(
111
+ `Can't find any doc with id=${docId}.\nAvailable doc ids:\n- ${Object.keys(
112
+ docsById,
113
+ ).join('\n- ')}`,
114
+ );
115
+ }
116
+ return doc;
117
+ };
118
+
103
119
  /**
104
120
  * Step 1. Extract the docs that are in the autogen dir.
105
121
  */
@@ -163,12 +179,11 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
163
179
  * (From a record to an array of items, akin to normalizing shorthand)
164
180
  */
165
181
  function generateSidebar(fsModel: Dir): Promise<WithPosition<SidebarItem>[]> {
166
- const docsById = keyBy(allDocs, (doc) => doc.id);
167
182
  function createDocItem(id: string): WithPosition<SidebarItemDoc> {
168
183
  const {
169
184
  sidebarPosition: position,
170
185
  frontMatter: {sidebar_label: label, sidebar_class_name: className},
171
- } = docsById[id];
186
+ } = getDoc(id);
172
187
  return {
173
188
  type: 'doc',
174
189
  id,
@@ -187,6 +202,57 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
187
202
  const categoryMetadata = await readCategoryMetadataFile(categoryPath);
188
203
  const className = categoryMetadata?.className;
189
204
  const {filename, numberPrefix} = numberPrefixParser(folderName);
205
+ const allItems = await Promise.all(
206
+ Object.entries(dir).map(([key, content]) =>
207
+ dirToItem(content, key, `${fullPath}/${key}`),
208
+ ),
209
+ );
210
+
211
+ // Try to match a doc inside the category folder,
212
+ // using the "local id" (myDoc) or "qualified id" (dirName/myDoc)
213
+ function findDocByLocalId(localId: string): SidebarItemDoc | undefined {
214
+ return allItems.find(
215
+ (item) => item.type === 'doc' && getLocalDocId(item.id) === localId,
216
+ ) as SidebarItemDoc | undefined;
217
+ }
218
+
219
+ function findConventionalCategoryDocLink(): SidebarItemDoc | undefined {
220
+ return allItems.find(
221
+ (item) =>
222
+ item.type === 'doc' && isConventionalDocIndex(getDoc(item.id)),
223
+ ) as SidebarItemDoc | undefined;
224
+ }
225
+
226
+ function getCategoryLinkedDocId(): string | undefined {
227
+ const link = categoryMetadata?.link;
228
+ if (link) {
229
+ if (link.type === 'doc') {
230
+ return findDocByLocalId(link.id)?.id || getDoc(link.id).id;
231
+ } else {
232
+ // We don't continue for other link types on purpose!
233
+ // IE if user decide to use type "generated-index", we should not pick a README.md file as the linked doc
234
+ return undefined;
235
+ }
236
+ }
237
+ // Apply default convention to pick index.md, README.md or <categoryName>.md as the category doc
238
+ return findConventionalCategoryDocLink()?.id;
239
+ }
240
+
241
+ const categoryLinkedDocId = getCategoryLinkedDocId();
242
+
243
+ const link: SidebarItemCategoryLink | undefined = categoryLinkedDocId
244
+ ? {
245
+ type: 'doc',
246
+ id: categoryLinkedDocId, // We "remap" a potentially "local id" to a "qualified id"
247
+ }
248
+ : // TODO typing issue
249
+ (categoryMetadata?.link as SidebarItemCategoryLink | undefined);
250
+
251
+ // If a doc is linked, remove it from the category subItems
252
+ const items = allItems.filter(
253
+ (item) => !(item.type === 'doc' && item.id === categoryLinkedDocId),
254
+ );
255
+
190
256
  return {
191
257
  type: 'category',
192
258
  label: categoryMetadata?.label ?? filename,
@@ -195,11 +261,8 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
195
261
  collapsed: categoryMetadata?.collapsed ?? options.sidebarCollapsed,
196
262
  position: categoryMetadata?.position ?? numberPrefix,
197
263
  ...(className !== undefined && {className}),
198
- items: await Promise.all(
199
- Object.entries(dir).map(([key, content]) =>
200
- dirToItem(content, key, `${fullPath}/${key}`),
201
- ),
202
- ),
264
+ items,
265
+ ...(link && {link}),
203
266
  };
204
267
  }
205
268
  async function dirToItem(