@lukoweb/apitogo 0.1.18 → 0.1.20

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.
@@ -1,352 +1,352 @@
1
- import path from "node:path";
2
- import { glob } from "glob";
3
- import type { RootContent } from "hast";
4
- import type { LucideIcon } from "lucide-react";
5
- import type { Heading, PhrasingContent } from "mdast";
6
- import { fromMarkdown } from "mdast-util-from-markdown";
7
- import { mdxFromMarkdown } from "mdast-util-mdx";
8
- import type { MdxJsxTextElement } from "mdast-util-mdx-jsx";
9
- import { mdxjs } from "micromark-extension-mdxjs";
10
- import type { SortableType } from "../../lib/navigation/applyRules.js";
11
- import { readFrontmatter } from "../../lib/util/readFrontmatter.js";
12
- import type { ConfigWithMeta } from "../loader.js";
13
- import type {
14
- InputNavigationCategory,
15
- InputNavigationCategoryLinkDoc,
16
- InputNavigationCustomPage,
17
- InputNavigationDoc,
18
- InputNavigationFilter,
19
- InputNavigationItem,
20
- InputNavigationLink,
21
- InputNavigationSection,
22
- InputNavigationSeparator,
23
- NavigationInsertRule,
24
- NavigationModifyRule,
25
- NavigationMoveRule,
26
- NavigationRemoveRule,
27
- NavigationRule,
28
- NavigationSortRule,
29
- } from "./InputNavigationSchema.js";
30
- import { DocsConfigSchema } from "./ZudokuConfig.js";
31
-
32
- type ReplaceFields<Base, Overrides> = Omit<Base, keyof Overrides> & Overrides;
33
- // string icons will be transformed to `LucideIcon` in `vite/plugin-navigation.ts`
34
- type ResolvedIcon = { icon?: LucideIcon | string };
35
-
36
- // `doc` items can have string shorthands, but this resolver will resolve them to the full type
37
- type FinalNavigationDoc = Extract<InputNavigationDoc, { type: "doc" }>;
38
- type FinalNavigationCategoryLinkDoc = Extract<
39
- InputNavigationCategoryLinkDoc,
40
- { type: "doc" }
41
- >;
42
-
43
- export type NavigationDoc = ReplaceFields<
44
- FinalNavigationDoc,
45
- {
46
- label: string;
47
- categoryLabel?: string;
48
- path: string;
49
- rich?: RootContent[];
50
- } & ResolvedIcon
51
- >;
52
-
53
- export type NavigationLink = ReplaceFields<InputNavigationLink, ResolvedIcon>;
54
-
55
- export type NavigationCategoryLinkDoc = ReplaceFields<
56
- FinalNavigationCategoryLinkDoc,
57
- { label: string; path: string } & ResolvedIcon
58
- >;
59
-
60
- export type NavigationCategory = ReplaceFields<
61
- InputNavigationCategory,
62
- {
63
- items: NavigationItem[];
64
- link?: NavigationCategoryLinkDoc;
65
- } & ResolvedIcon
66
- >;
67
- export type NavigationCustomPage = ReplaceFields<
68
- InputNavigationCustomPage,
69
- ResolvedIcon
70
- >;
71
-
72
- export type NavigationSeparator = InputNavigationSeparator & { label: string };
73
-
74
- export type NavigationSection = InputNavigationSection;
75
-
76
- export type NavigationFilter = InputNavigationFilter & { label: string };
77
-
78
- export type NavigationItem =
79
- | NavigationDoc
80
- | NavigationLink
81
- | NavigationCategory
82
- | NavigationCustomPage
83
- | NavigationSeparator
84
- | NavigationSection
85
- | NavigationFilter;
86
-
87
- export type SortableNavigationItem = Extract<
88
- NavigationItem,
89
- { type: SortableType }
90
- >;
91
-
92
- export type Navigation = NavigationItem[];
93
-
94
- const extractTitleFromContent = (content: string): string | undefined =>
95
- content.match(/^\s*#\s(.*)$/m)?.at(1);
96
-
97
- type MdxPhrasingContent = PhrasingContent | MdxJsxTextElement;
98
-
99
- const isMdxJsxElement = (node: MdxPhrasingContent): node is MdxJsxTextElement =>
100
- node.type === "mdxJsxTextElement";
101
-
102
- const mdastToString = (node: MdxPhrasingContent | Heading): string => {
103
- if ("value" in node && typeof node.value === "string") return node.value;
104
- if ("children" in node && Array.isArray(node.children)) {
105
- return node.children
106
- .map((c) => mdastToString(c as MdxPhrasingContent))
107
- .join("");
108
- }
109
-
110
- return "";
111
- };
112
-
113
- // Extract rich H1 heading content from MDX. Returns AST nodes only when H1 contains JSX elements.
114
- const extractRichH1 = (content: string) => {
115
- try {
116
- const mdast = fromMarkdown(content, {
117
- extensions: [mdxjs()],
118
- mdastExtensions: [mdxFromMarkdown()],
119
- // biome-ignore lint/suspicious/noExplicitAny: mdast-util-from-markdown has type incompatibilities between versions
120
- } as any);
121
-
122
- const h1 = mdast.children.find(
123
- (node): node is Heading => node.type === "heading" && node.depth === 1,
124
- );
125
-
126
- if (!h1) return undefined;
127
-
128
- const children = h1.children as MdxPhrasingContent[];
129
- const hasJsx = children.some(isMdxJsxElement);
130
-
131
- // Extract all text content including from emphasis/strong/links
132
- const label = mdastToString(h1).trim();
133
-
134
- // Note: rich only contains MDAST nodes. RichText handles text and mdxJsxTextElement,
135
- // but markdown formatting (strong/emphasis/link) in H1 won't render styled.
136
- return hasJsx ? { label, rich: children as RootContent[] } : { label };
137
- } catch {
138
- return undefined;
139
- }
140
- };
141
-
142
- const isNavigationItem = (item: unknown): item is NavigationItem =>
143
- item !== undefined;
144
-
145
- const toPosixPath = (filePath: string) =>
146
- filePath.split(path.win32.sep).join(path.posix.sep);
147
-
148
- export type ResolvedNavigationInsertRule = Omit<
149
- NavigationInsertRule,
150
- "items"
151
- > & { items: NavigationItem[] };
152
-
153
- export type ResolvedNavigationRule =
154
- | NavigationModifyRule
155
- | NavigationRemoveRule
156
- | ResolvedNavigationInsertRule
157
- | NavigationSortRule
158
- | NavigationMoveRule;
159
-
160
- export class NavigationResolver {
161
- private rootDir: string;
162
- private globPatterns: string[];
163
- private globFiles: string[] = [];
164
- private items: InputNavigationItem[] = [];
165
- private itemIndex = 0;
166
-
167
- constructor(config: ConfigWithMeta) {
168
- this.rootDir = config.__meta.rootDir;
169
- this.globPatterns = DocsConfigSchema.parse(config.docs ?? {}).files;
170
- this.items = config.navigation ?? [];
171
- }
172
-
173
- async initialize() {
174
- if (this.globFiles.length > 0) return;
175
-
176
- this.globFiles = await glob(this.globPatterns, {
177
- root: this.rootDir,
178
- ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"],
179
- }).then((files) => files.map(toPosixPath));
180
- }
181
-
182
- async resolve() {
183
- await this.initialize();
184
-
185
- const resolvedItems = await Promise.all(
186
- this.items.map((item) => this.resolveItem(item)),
187
- );
188
-
189
- return resolvedItems.filter(isNavigationItem);
190
- }
191
-
192
- async resolveRules(
193
- rules: NavigationRule[],
194
- ): Promise<ResolvedNavigationRule[]> {
195
- await this.initialize();
196
-
197
- return Promise.all(
198
- rules.map(async (rule) => {
199
- if (rule.type === "insert") {
200
- const items = await Promise.all(
201
- rule.items.map((item) => this.resolveItem(item)),
202
- ).then((items) => items.filter(isNavigationItem));
203
-
204
- return { ...rule, items };
205
- }
206
- return rule;
207
- }),
208
- );
209
- }
210
-
211
- private async resolveDoc(
212
- filePath: string,
213
- categoryLabel?: string,
214
- ): Promise<NavigationDoc | undefined> {
215
- const fileNoExt = toPosixPath(filePath).replace(/\.mdx?$/, "");
216
-
217
- const foundMatches = this.globFiles.find(
218
- (file) =>
219
- file.endsWith(`${fileNoExt}.md`) || file.endsWith(`${fileNoExt}.mdx`),
220
- );
221
-
222
- if (!foundMatches) {
223
- throw new Error(
224
- `File not found for document '${filePath}'. Navigation items of type 'doc' must point to a valid .md or .mdx file. Do you mean 'link' or 'custom-page'? Check navigation configuration documentation for more information: https://docs.apitogo.com/docs/configuration/navigation`,
225
- );
226
- }
227
-
228
- const { data, content } = await readFrontmatter(foundMatches);
229
-
230
- // Skip draft documents in production mode
231
- if (process.env.NODE_ENV !== "development" && data.draft === true) {
232
- return undefined;
233
- }
234
-
235
- const richH1 = extractRichH1(content);
236
-
237
- const label =
238
- data.navigation_label ??
239
- data.sidebar_label ??
240
- data.title ??
241
- richH1?.label ??
242
- extractTitleFromContent(content) ??
243
- filePath;
244
-
245
- const icon = data.navigation_icon ?? data.sidebar_icon;
246
-
247
- const doc = {
248
- type: "doc",
249
- file: filePath,
250
- label,
251
- icon,
252
- display: data.navigation_display,
253
- categoryLabel,
254
- path: fileNoExt,
255
- rich: richH1?.rich,
256
- } satisfies NavigationDoc;
257
-
258
- return doc;
259
- }
260
-
261
- private async resolveLink(
262
- file: string,
263
- ): Promise<NavigationCategoryLinkDoc | undefined> {
264
- const doc = await this.resolveDoc(file);
265
-
266
- return doc
267
- ? { type: "doc", file, label: doc.label, icon: doc.icon, path: doc.path }
268
- : undefined;
269
- }
270
-
271
- private async resolveItemCategoryLinkDoc(
272
- item: string | InputNavigationCategoryLinkDoc,
273
- ): Promise<NavigationCategoryLinkDoc | undefined> {
274
- if (typeof item === "string") {
275
- return this.resolveLink(item);
276
- }
277
-
278
- const doc = await this.resolveDoc(item.file);
279
- return doc
280
- ? {
281
- ...item,
282
- label: doc.label,
283
- icon: doc.icon,
284
- path: item.path ?? doc.path,
285
- }
286
- : undefined;
287
- }
288
-
289
- private async resolveNavigationItemDoc(
290
- item: string | InputNavigationDoc,
291
- categoryLabel?: string,
292
- ): Promise<NavigationDoc | undefined> {
293
- if (typeof item === "string") {
294
- return this.resolveDoc(item, categoryLabel);
295
- }
296
-
297
- const doc = await this.resolveDoc(item.file, categoryLabel);
298
- return doc ? { ...doc, ...item, path: item.path ?? doc.path } : undefined;
299
- }
300
-
301
- private async resolveItem(
302
- item: InputNavigationItem,
303
- categoryLabel?: string,
304
- ): Promise<NavigationItem | undefined> {
305
- if (typeof item === "string") {
306
- return this.resolveDoc(item, categoryLabel);
307
- }
308
-
309
- switch (item.type) {
310
- case "doc":
311
- return this.resolveNavigationItemDoc(item, categoryLabel);
312
- case "link":
313
- case "custom-page":
314
- case "section":
315
- return item;
316
- case "separator":
317
- return { ...item, label: `separator-${this.itemIndex++}` };
318
- case "filter":
319
- return { ...item, label: `filter-${this.itemIndex++}` };
320
- case "category": {
321
- const categoryItem = item;
322
-
323
- const items = (
324
- await Promise.all(
325
- (categoryItem.items as InputNavigationItem[]).map((subItem) =>
326
- this.resolveItem(subItem, categoryItem.label),
327
- ),
328
- )
329
- ).filter(isNavigationItem);
330
-
331
- const resolvedLink = categoryItem.link
332
- ? await this.resolveItemCategoryLinkDoc(categoryItem.link)
333
- : undefined;
334
-
335
- // Filter out empty categories (no items and no link) in production
336
- if (
337
- process.env.NODE_ENV !== "development" &&
338
- items.length === 0 &&
339
- !resolvedLink
340
- ) {
341
- return undefined;
342
- }
343
-
344
- return {
345
- ...categoryItem,
346
- items,
347
- link: resolvedLink,
348
- };
349
- }
350
- }
351
- }
352
- }
1
+ import path from "node:path";
2
+ import { glob } from "glob";
3
+ import type { RootContent } from "hast";
4
+ import type { LucideIcon } from "lucide-react";
5
+ import type { Heading, PhrasingContent } from "mdast";
6
+ import { fromMarkdown } from "mdast-util-from-markdown";
7
+ import { mdxFromMarkdown } from "mdast-util-mdx";
8
+ import type { MdxJsxTextElement } from "mdast-util-mdx-jsx";
9
+ import { mdxjs } from "micromark-extension-mdxjs";
10
+ import type { SortableType } from "../../lib/navigation/applyRules.js";
11
+ import { readFrontmatter } from "../../lib/util/readFrontmatter.js";
12
+ import type { ConfigWithMeta } from "../loader.js";
13
+ import type {
14
+ InputNavigationCategory,
15
+ InputNavigationCategoryLinkDoc,
16
+ InputNavigationCustomPage,
17
+ InputNavigationDoc,
18
+ InputNavigationFilter,
19
+ InputNavigationItem,
20
+ InputNavigationLink,
21
+ InputNavigationSection,
22
+ InputNavigationSeparator,
23
+ NavigationInsertRule,
24
+ NavigationModifyRule,
25
+ NavigationMoveRule,
26
+ NavigationRemoveRule,
27
+ NavigationRule,
28
+ NavigationSortRule,
29
+ } from "./InputNavigationSchema.js";
30
+ import { DocsConfigSchema } from "./ZudokuConfig.js";
31
+
32
+ type ReplaceFields<Base, Overrides> = Omit<Base, keyof Overrides> & Overrides;
33
+ // string icons will be transformed to `LucideIcon` in `vite/plugin-navigation.ts`
34
+ type ResolvedIcon = { icon?: LucideIcon | string };
35
+
36
+ // `doc` items can have string shorthands, but this resolver will resolve them to the full type
37
+ type FinalNavigationDoc = Extract<InputNavigationDoc, { type: "doc" }>;
38
+ type FinalNavigationCategoryLinkDoc = Extract<
39
+ InputNavigationCategoryLinkDoc,
40
+ { type: "doc" }
41
+ >;
42
+
43
+ export type NavigationDoc = ReplaceFields<
44
+ FinalNavigationDoc,
45
+ {
46
+ label: string;
47
+ categoryLabel?: string;
48
+ path: string;
49
+ rich?: RootContent[];
50
+ } & ResolvedIcon
51
+ >;
52
+
53
+ export type NavigationLink = ReplaceFields<InputNavigationLink, ResolvedIcon>;
54
+
55
+ export type NavigationCategoryLinkDoc = ReplaceFields<
56
+ FinalNavigationCategoryLinkDoc,
57
+ { label: string; path: string } & ResolvedIcon
58
+ >;
59
+
60
+ export type NavigationCategory = ReplaceFields<
61
+ InputNavigationCategory,
62
+ {
63
+ items: NavigationItem[];
64
+ link?: NavigationCategoryLinkDoc;
65
+ } & ResolvedIcon
66
+ >;
67
+ export type NavigationCustomPage = ReplaceFields<
68
+ InputNavigationCustomPage,
69
+ ResolvedIcon
70
+ >;
71
+
72
+ export type NavigationSeparator = InputNavigationSeparator & { label: string };
73
+
74
+ export type NavigationSection = InputNavigationSection;
75
+
76
+ export type NavigationFilter = InputNavigationFilter & { label: string };
77
+
78
+ export type NavigationItem =
79
+ | NavigationDoc
80
+ | NavigationLink
81
+ | NavigationCategory
82
+ | NavigationCustomPage
83
+ | NavigationSeparator
84
+ | NavigationSection
85
+ | NavigationFilter;
86
+
87
+ export type SortableNavigationItem = Extract<
88
+ NavigationItem,
89
+ { type: SortableType }
90
+ >;
91
+
92
+ export type Navigation = NavigationItem[];
93
+
94
+ const extractTitleFromContent = (content: string): string | undefined =>
95
+ content.match(/^\s*#\s(.*)$/m)?.at(1);
96
+
97
+ type MdxPhrasingContent = PhrasingContent | MdxJsxTextElement;
98
+
99
+ const isMdxJsxElement = (node: MdxPhrasingContent): node is MdxJsxTextElement =>
100
+ node.type === "mdxJsxTextElement";
101
+
102
+ const mdastToString = (node: MdxPhrasingContent | Heading): string => {
103
+ if ("value" in node && typeof node.value === "string") return node.value;
104
+ if ("children" in node && Array.isArray(node.children)) {
105
+ return node.children
106
+ .map((c) => mdastToString(c as MdxPhrasingContent))
107
+ .join("");
108
+ }
109
+
110
+ return "";
111
+ };
112
+
113
+ // Extract rich H1 heading content from MDX. Returns AST nodes only when H1 contains JSX elements.
114
+ const extractRichH1 = (content: string) => {
115
+ try {
116
+ const mdast = fromMarkdown(content, {
117
+ extensions: [mdxjs()],
118
+ mdastExtensions: [mdxFromMarkdown()],
119
+ // biome-ignore lint/suspicious/noExplicitAny: mdast-util-from-markdown has type incompatibilities between versions
120
+ } as any);
121
+
122
+ const h1 = mdast.children.find(
123
+ (node): node is Heading => node.type === "heading" && node.depth === 1,
124
+ );
125
+
126
+ if (!h1) return undefined;
127
+
128
+ const children = h1.children as MdxPhrasingContent[];
129
+ const hasJsx = children.some(isMdxJsxElement);
130
+
131
+ // Extract all text content including from emphasis/strong/links
132
+ const label = mdastToString(h1).trim();
133
+
134
+ // Note: rich only contains MDAST nodes. RichText handles text and mdxJsxTextElement,
135
+ // but markdown formatting (strong/emphasis/link) in H1 won't render styled.
136
+ return hasJsx ? { label, rich: children as RootContent[] } : { label };
137
+ } catch {
138
+ return undefined;
139
+ }
140
+ };
141
+
142
+ const isNavigationItem = (item: unknown): item is NavigationItem =>
143
+ item !== undefined;
144
+
145
+ const toPosixPath = (filePath: string) =>
146
+ filePath.split(path.win32.sep).join(path.posix.sep);
147
+
148
+ export type ResolvedNavigationInsertRule = Omit<
149
+ NavigationInsertRule,
150
+ "items"
151
+ > & { items: NavigationItem[] };
152
+
153
+ export type ResolvedNavigationRule =
154
+ | NavigationModifyRule
155
+ | NavigationRemoveRule
156
+ | ResolvedNavigationInsertRule
157
+ | NavigationSortRule
158
+ | NavigationMoveRule;
159
+
160
+ export class NavigationResolver {
161
+ private rootDir: string;
162
+ private globPatterns: string[];
163
+ private globFiles: string[] = [];
164
+ private items: InputNavigationItem[] = [];
165
+ private itemIndex = 0;
166
+
167
+ constructor(config: ConfigWithMeta) {
168
+ this.rootDir = config.__meta.rootDir;
169
+ this.globPatterns = DocsConfigSchema.parse(config.docs ?? {}).files;
170
+ this.items = config.navigation ?? [];
171
+ }
172
+
173
+ async initialize() {
174
+ if (this.globFiles.length > 0) return;
175
+
176
+ this.globFiles = await glob(this.globPatterns, {
177
+ root: this.rootDir,
178
+ ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"],
179
+ }).then((files) => files.map(toPosixPath));
180
+ }
181
+
182
+ async resolve() {
183
+ await this.initialize();
184
+
185
+ const resolvedItems = await Promise.all(
186
+ this.items.map((item) => this.resolveItem(item)),
187
+ );
188
+
189
+ return resolvedItems.filter(isNavigationItem);
190
+ }
191
+
192
+ async resolveRules(
193
+ rules: NavigationRule[],
194
+ ): Promise<ResolvedNavigationRule[]> {
195
+ await this.initialize();
196
+
197
+ return Promise.all(
198
+ rules.map(async (rule) => {
199
+ if (rule.type === "insert") {
200
+ const items = await Promise.all(
201
+ rule.items.map((item) => this.resolveItem(item)),
202
+ ).then((items) => items.filter(isNavigationItem));
203
+
204
+ return { ...rule, items };
205
+ }
206
+ return rule;
207
+ }),
208
+ );
209
+ }
210
+
211
+ private async resolveDoc(
212
+ filePath: string,
213
+ categoryLabel?: string,
214
+ ): Promise<NavigationDoc | undefined> {
215
+ const fileNoExt = toPosixPath(filePath).replace(/\.mdx?$/, "");
216
+
217
+ const foundMatches = this.globFiles.find(
218
+ (file) =>
219
+ file.endsWith(`${fileNoExt}.md`) || file.endsWith(`${fileNoExt}.mdx`),
220
+ );
221
+
222
+ if (!foundMatches) {
223
+ throw new Error(
224
+ `File not found for document '${filePath}'. Navigation items of type 'doc' must point to a valid .md or .mdx file. Do you mean 'link' or 'custom-page'? Check navigation configuration documentation for more information: https://docs.apitogo.com/docs/configuration/navigation`,
225
+ );
226
+ }
227
+
228
+ const { data, content } = await readFrontmatter(foundMatches);
229
+
230
+ // Skip draft documents in production mode
231
+ if (process.env.NODE_ENV !== "development" && data.draft === true) {
232
+ return undefined;
233
+ }
234
+
235
+ const richH1 = extractRichH1(content);
236
+
237
+ const label =
238
+ data.navigation_label ??
239
+ data.sidebar_label ??
240
+ data.title ??
241
+ richH1?.label ??
242
+ extractTitleFromContent(content) ??
243
+ filePath;
244
+
245
+ const icon = data.navigation_icon ?? data.sidebar_icon;
246
+
247
+ const doc = {
248
+ type: "doc",
249
+ file: filePath,
250
+ label,
251
+ icon,
252
+ display: data.navigation_display,
253
+ categoryLabel,
254
+ path: fileNoExt,
255
+ rich: richH1?.rich,
256
+ } satisfies NavigationDoc;
257
+
258
+ return doc;
259
+ }
260
+
261
+ private async resolveLink(
262
+ file: string,
263
+ ): Promise<NavigationCategoryLinkDoc | undefined> {
264
+ const doc = await this.resolveDoc(file);
265
+
266
+ return doc
267
+ ? { type: "doc", file, label: doc.label, icon: doc.icon, path: doc.path }
268
+ : undefined;
269
+ }
270
+
271
+ private async resolveItemCategoryLinkDoc(
272
+ item: string | InputNavigationCategoryLinkDoc,
273
+ ): Promise<NavigationCategoryLinkDoc | undefined> {
274
+ if (typeof item === "string") {
275
+ return this.resolveLink(item);
276
+ }
277
+
278
+ const doc = await this.resolveDoc(item.file);
279
+ return doc
280
+ ? {
281
+ ...item,
282
+ label: doc.label,
283
+ icon: doc.icon,
284
+ path: item.path ?? doc.path,
285
+ }
286
+ : undefined;
287
+ }
288
+
289
+ private async resolveNavigationItemDoc(
290
+ item: string | InputNavigationDoc,
291
+ categoryLabel?: string,
292
+ ): Promise<NavigationDoc | undefined> {
293
+ if (typeof item === "string") {
294
+ return this.resolveDoc(item, categoryLabel);
295
+ }
296
+
297
+ const doc = await this.resolveDoc(item.file, categoryLabel);
298
+ return doc ? { ...doc, ...item, path: item.path ?? doc.path } : undefined;
299
+ }
300
+
301
+ private async resolveItem(
302
+ item: InputNavigationItem,
303
+ categoryLabel?: string,
304
+ ): Promise<NavigationItem | undefined> {
305
+ if (typeof item === "string") {
306
+ return this.resolveDoc(item, categoryLabel);
307
+ }
308
+
309
+ switch (item.type) {
310
+ case "doc":
311
+ return this.resolveNavigationItemDoc(item, categoryLabel);
312
+ case "link":
313
+ case "custom-page":
314
+ case "section":
315
+ return item;
316
+ case "separator":
317
+ return { ...item, label: `separator-${this.itemIndex++}` };
318
+ case "filter":
319
+ return { ...item, label: `filter-${this.itemIndex++}` };
320
+ case "category": {
321
+ const categoryItem = item;
322
+
323
+ const items = (
324
+ await Promise.all(
325
+ (categoryItem.items as InputNavigationItem[]).map((subItem) =>
326
+ this.resolveItem(subItem, categoryItem.label),
327
+ ),
328
+ )
329
+ ).filter(isNavigationItem);
330
+
331
+ const resolvedLink = categoryItem.link
332
+ ? await this.resolveItemCategoryLinkDoc(categoryItem.link)
333
+ : undefined;
334
+
335
+ // Filter out empty categories (no items and no link) in production
336
+ if (
337
+ process.env.NODE_ENV !== "development" &&
338
+ items.length === 0 &&
339
+ !resolvedLink
340
+ ) {
341
+ return undefined;
342
+ }
343
+
344
+ return {
345
+ ...categoryItem,
346
+ items,
347
+ link: resolvedLink,
348
+ };
349
+ }
350
+ }
351
+ }
352
+ }