@apimatic/cli 2.0.0-beta.1 → 2.0.0-beta.3

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 (89) hide show
  1. package/README.md +19 -6
  2. package/apimatic.schema.json +1 -1
  3. package/lib/actions/plugin/generate.d.ts +0 -2
  4. package/lib/actions/plugin/generate.js +5 -8
  5. package/lib/actions/plugin/generate.js.map +1 -1
  6. package/lib/actions/portal/serve.js +4 -3
  7. package/lib/actions/portal/serve.js.map +1 -1
  8. package/lib/actions/quickstart.js +3 -4
  9. package/lib/actions/quickstart.js.map +1 -1
  10. package/lib/actions/sdk/generate.js +16 -3
  11. package/lib/actions/sdk/generate.js.map +1 -1
  12. package/lib/actions/sdk/publish.js +6 -2
  13. package/lib/actions/sdk/publish.js.map +1 -1
  14. package/lib/infrastructure/services/portal-artifacts-service.js +6 -5
  15. package/lib/infrastructure/services/portal-artifacts-service.js.map +1 -1
  16. package/lib/infrastructure/zip-service.d.ts +6 -2
  17. package/lib/infrastructure/zip-service.js +42 -24
  18. package/lib/infrastructure/zip-service.js.map +1 -1
  19. package/lib/prompts/plugin/generate.d.ts +3 -3
  20. package/lib/prompts/plugin/generate.js +19 -11
  21. package/lib/prompts/plugin/generate.js.map +1 -1
  22. package/lib/prompts/portal/serve.js +9 -11
  23. package/lib/prompts/portal/serve.js.map +1 -1
  24. package/lib/prompts/portal/source.d.ts +4 -2
  25. package/lib/prompts/portal/source.js +42 -8
  26. package/lib/prompts/portal/source.js.map +1 -1
  27. package/lib/prompts/quickstart.js +4 -10
  28. package/lib/prompts/quickstart.js.map +1 -1
  29. package/lib/prompts/sdk/generate.d.ts +5 -0
  30. package/lib/prompts/sdk/generate.js +10 -0
  31. package/lib/prompts/sdk/generate.js.map +1 -1
  32. package/lib/prompts/sdk/publish.d.ts +3 -0
  33. package/lib/prompts/sdk/publish.js +3 -0
  34. package/lib/prompts/sdk/publish.js.map +1 -1
  35. package/lib/types/apimatic-config/document.d.ts +4 -6
  36. package/lib/types/apimatic-config/document.js +4 -10
  37. package/lib/types/apimatic-config/document.js.map +1 -1
  38. package/lib/types/file/file-problem.d.ts +4 -0
  39. package/lib/types/file/file-problem.js +2 -0
  40. package/lib/types/file/file-problem.js.map +1 -0
  41. package/lib/types/plugin/generation-problem.d.ts +10 -0
  42. package/lib/types/plugin/generation-problem.js +2 -0
  43. package/lib/types/plugin/generation-problem.js.map +1 -0
  44. package/lib/types/plugin-config-context.js +2 -1
  45. package/lib/types/plugin-config-context.js.map +1 -1
  46. package/lib/types/plugin-context.d.ts +3 -1
  47. package/lib/types/plugin-context.js +2 -1
  48. package/lib/types/plugin-context.js.map +1 -1
  49. package/lib/types/portal/content-notices.d.ts +11 -3
  50. package/lib/types/portal/content-tree.d.ts +4 -2
  51. package/lib/types/portal/content-tree.js +42 -12
  52. package/lib/types/portal/content-tree.js.map +1 -1
  53. package/lib/types/portal/generated-pages.d.ts +6 -6
  54. package/lib/types/portal/generated-pages.js +3 -3
  55. package/lib/types/portal/generated-pages.js.map +1 -1
  56. package/lib/types/portal/portal-navigation.d.ts +83 -12
  57. package/lib/types/portal/portal-navigation.js +276 -100
  58. package/lib/types/portal/portal-navigation.js.map +1 -1
  59. package/lib/types/portal/preview-content.js +15 -2
  60. package/lib/types/portal/preview-content.js.map +1 -1
  61. package/lib/types/portal-context.d.ts +1 -0
  62. package/lib/types/portal-context.js +17 -12
  63. package/lib/types/portal-context.js.map +1 -1
  64. package/lib/types/portal-source-context.js +7 -4
  65. package/lib/types/portal-source-context.js.map +1 -1
  66. package/lib/types/project-context.d.ts +2 -1
  67. package/lib/types/project-context.js +2 -2
  68. package/lib/types/project-context.js.map +1 -1
  69. package/lib/types/sdk-context.d.ts +7 -2
  70. package/lib/types/sdk-context.js +8 -6
  71. package/lib/types/sdk-context.js.map +1 -1
  72. package/lib/types/spec-context.d.ts +3 -1
  73. package/lib/types/spec-context.js +5 -4
  74. package/lib/types/spec-context.js.map +1 -1
  75. package/lib/types/temp-context.d.ts +5 -3
  76. package/lib/types/temp-context.js +7 -7
  77. package/lib/types/temp-context.js.map +1 -1
  78. package/oclif.manifest.json +1 -1
  79. package/package.json +7 -6
  80. package/portal-template/src/components/api-page.tsx +2 -6
  81. package/portal-template/src/components/example-layout.tsx +7 -7
  82. package/portal-template/src/components/usage-tabs.tsx +16 -36
  83. package/portal-template/src/lib/media-adapters.ts +3 -3
  84. package/portal-template/src/lib/navigation-schema.ts +13 -0
  85. package/portal-template/src/lib/navigation.ts +60 -117
  86. package/portal-template/src/lib/request-examples.ts +12 -28
  87. package/portal-template/src/lib/source.ts +3 -2
  88. package/portal-template/src/lib/tabs.ts +3 -2
  89. package/portal-template/src/components/code-block.tsx +0 -9
@@ -26,11 +26,7 @@ const REST_TOKEN = '...';
26
26
  /** The API reference, positioned as one node. */
27
27
  const API_REFERENCE_TOKEN = 'apimatic:api';
28
28
 
29
- /**
30
- * The folders the CLI generates, each a tab of its own, by the token that positions it and in
31
- * the order they take when the root `nav.json` names none of them. The CLI's
32
- * `GENERATED_SECTIONS` lists the same, and a test holds the two together.
33
- */
29
+ /** The folders the CLI generates, by the token that positions each; the CLI's `GENERATED_SECTIONS` lists the same. */
34
30
  export const GENERATED_SECTIONS = [
35
31
  { token: 'apimatic:sdks', folder: 'sdks' },
36
32
  { token: 'apimatic:plugin', folder: 'context-plugin' }
@@ -43,15 +39,15 @@ const API_REFERENCE_TITLE = 'API Reference';
43
39
  /** Where the home page is served, which is how its node is told apart from every other page. */
44
40
  const HOME_URL = docsRoute;
45
41
 
46
- /** Fixed, as tab matching goes by id after serialisation; Fumadocs' ids never start with a slash. */
47
- const HOME_TAB_ID = '/tab/home';
48
-
49
42
  /** The Home tab's name when the root `nav.json` gives none, and the fallback home page's. */
50
43
  const HOME_NAME = 'Home';
51
44
 
52
45
  /** The node the fallback home page gets when there is no index page; see `withFallbackHomePage`. */
53
46
  const SYNTHETIC_HOME_ID = '/page/home';
54
47
 
48
+ /** The Home tab's id. Fixed, as tab matching goes by id after serialisation; Fumadocs' ids never start with a slash. */
49
+ const HOME_TAB_ID = '/tab/home';
50
+
55
51
  /**
56
52
  * The key the generated pages are passed to `loader()` under, which the storage stamps onto
57
53
  * every file of that source.
@@ -82,16 +78,10 @@ export function navigationTransformer<S extends ContentStorage>(): PageTreeTrans
82
78
  // folder whose only page is its index is not empty: the folder itself links to it.
83
79
  node.children = node.children.filter((child) => !isEmptyFolder(child));
84
80
 
85
- // The root is ordered even with no file of its own: naming nothing still puts the
86
- // generated sections and the API reference at their defaults, and Fumadocs' own order
87
- // would not. It sorts folders by path, so `api` lands above a user folder called anything
88
- // later in the alphabet, and a generated folder lands in the middle of the user's pages.
89
- // Below the root those defaults do not apply, so a folder with no file keeps Fumadocs'
90
- // order untouched, which is every tag folder of every specification.
81
+ // A directory with no order keeps Fumadocs' own, the root included.
91
82
  const settings = readSettings(this, folderPath);
92
- const order = settings?.pages;
93
- if (folderPath === '' || order !== undefined) {
94
- node.children = reorder(this, node, folderPath, order ?? []);
83
+ if (settings?.pages !== undefined) {
84
+ node.children = reorder(this, node, folderPath, settings.pages);
95
85
  }
96
86
  if (folderPath === apiBaseDir) {
97
87
  applyApiStructure(this, node);
@@ -105,7 +95,7 @@ export function navigationTransformer<S extends ContentStorage>(): PageTreeTrans
105
95
  };
106
96
  }
107
97
 
108
- /** A tab per folder the root `nav.json` lists, the API reference and each generated folder; the rest is Home. */
98
+ /** A tab per entry of the root `nav.json`'s `tabs`, in its order; everything else is Home. */
109
99
  export function tabsTransformer<S extends ContentStorage>(): PageTreeTransformer<S> {
110
100
  return {
111
101
  root(root) {
@@ -119,6 +109,8 @@ export function tabsTransformer<S extends ContentStorage>(): PageTreeTransformer
119
109
  interface NavigationSettings {
120
110
  pages: string[] | undefined;
121
111
  title: string | undefined;
112
+ /** Read at the root only; the CLI refuses it anywhere else. */
113
+ tabs: string[] | undefined;
122
114
  }
123
115
 
124
116
  function readSettings(context: NavigationContext, folderPath: string): NavigationSettings | undefined {
@@ -130,41 +122,36 @@ function readSettings(context: NavigationContext, folderPath: string): Navigatio
130
122
  return undefined;
131
123
  }
132
124
 
133
- const { pages, title } = file.data as { pages?: unknown; title?: unknown };
125
+ const { pages, tabs, title } = file.data as { pages?: unknown; tabs?: unknown; title?: unknown };
134
126
  // A half-typed title reloads to here as an empty string, which would blank the folder in
135
127
  // the sidebar with nothing to click. The CLI refuses one; the preview keeps the default
136
128
  // name until the file is worth reading again.
137
129
  const named = typeof title === 'string' ? title.trim() : '';
138
130
  return {
139
- pages: Array.isArray(pages) ? pages.filter((entry): entry is string => typeof entry === 'string') : undefined,
131
+ pages: stringsOf(pages),
132
+ tabs: stringsOf(tabs),
140
133
  title: named.length > 0 ? named : undefined
141
134
  };
142
135
  }
143
136
 
144
- function isTabFolder(rootSettings: NavigationSettings | undefined, folder: Folder): boolean {
145
- const folderPath = folder.$ref?.folder;
146
- return (
147
- folderPath !== undefined &&
148
- folderPath !== apiBaseDir &&
149
- lists(rootSettings, folderPath) &&
150
- // The home page is the Home tab's, whichever `(group)` folder serves it.
151
- !containsUrl([folder], HOME_URL)
152
- );
137
+ function stringsOf(entries: unknown): string[] | undefined {
138
+ return Array.isArray(entries) ? entries.filter((entry): entry is string => typeof entry === 'string') : undefined;
153
139
  }
154
140
 
141
+ /** The nodes the root `tabs` names become the tabs, in its order; everything else is Home. */
155
142
  function groupIntoTabs(context: NavigationContext, children: Node[]): Node[] {
156
143
  const settings = readSettings(context, '');
144
+ const remaining = new Set(children);
157
145
  const tabs: Folder[] = [];
158
- const loose: Node[] = [];
159
-
160
- for (const child of children) {
161
- if (
162
- child.type === 'folder' &&
163
- (isApiReference(child) || isTabFolder(settings, child) || isInjected(context, child))
164
- ) {
165
- tabs.push(asTab(child));
166
- } else {
167
- loose.push(child);
146
+
147
+ // Resolved as `reorder` resolves an entry, before the tabs lose their `$ref`. A page is no
148
+ // tab, and the home page is the Home tab's whichever `(group)` folder serves it; the CLI
149
+ // refuses both entries, and an entry naming nothing is left to it as well.
150
+ for (const raw of settings?.tabs ?? []) {
151
+ const node = nodeNamed(context, [...remaining], '', raw.trim());
152
+ if (node?.type === 'folder' && !containsUrl([node], HOME_URL)) {
153
+ remaining.delete(node);
154
+ tabs.push(asTab(node));
168
155
  }
169
156
  }
170
157
 
@@ -181,7 +168,7 @@ function groupIntoTabs(context: NavigationContext, children: Node[]): Node[] {
181
168
  $id: HOME_TAB_ID,
182
169
  name: settings?.title ?? HOME_NAME,
183
170
  root: true,
184
- children: withFallbackHomePage(loose)
171
+ children: withFallbackHomePage([...remaining])
185
172
  };
186
173
  // Home opens the site, so it leads wherever the file lists its pages; `index` orders them only.
187
174
  return [home, ...tabs];
@@ -209,10 +196,6 @@ function asTab(folder: Folder): Folder {
209
196
  return folder;
210
197
  }
211
198
 
212
- function lists(settings: NavigationSettings | undefined, name: string): boolean {
213
- return settings?.pages?.some((entry) => entry.trim() === name) ?? false;
214
- }
215
-
216
199
  /**
217
200
  * Titles the wrapper and, for a single specification, lifts its section away.
218
201
  *
@@ -268,66 +251,51 @@ function firstPageIn(folder: Folder): Node | undefined {
268
251
  return undefined;
269
252
  }
270
253
 
254
+ /**
255
+ * `node`'s children with the ones `order` names first, in its order, and the rest where its
256
+ * rest token stands, or after everything named when it has none. The rest keep Fumadocs' own
257
+ * order: pages before folders, each by path.
258
+ */
271
259
  function reorder(context: NavigationContext, node: Folder, folderPath: string, order: string[]): Node[] {
272
260
  const remaining = new Set(node.children);
273
- // Asked once per child: the token, both bands and the anchor all want to know, and each
274
- // answer is a storage read.
275
- const injectedChildren = new Set(node.children.filter((child) => isInjected(context, child)));
276
- const isInjectedChild = (child: Node): boolean => injectedChildren.has(child);
277
261
  const named: Node[] = [];
278
262
  let restIndex: number | undefined;
279
263
 
280
- const claim = (child: Node | undefined): void => {
281
- if (child !== undefined) {
282
- remaining.delete(child);
283
- named.push(child);
284
- }
285
- };
286
-
287
- // Every token names a node that lives at the content root, and the CLI refuses each one
288
- // anywhere else. Honouring them in a nested directory would let the two halves of the
289
- // format disagree about a file only one of them had rejected.
290
- const isContentRoot = folderPath === '';
291
-
292
264
  for (const raw of order) {
293
265
  const entry = raw.trim();
294
- const section = GENERATED_SECTIONS.find((candidate) => candidate.token === entry);
295
266
  if (entry === REST_TOKEN) {
296
267
  restIndex = named.length;
297
- } else if (entry === API_REFERENCE_TOKEN) {
298
- if (isContentRoot) {
299
- claim([...remaining].find((child) => isApiReference(child)));
300
- }
301
- } else if (section !== undefined) {
302
- if (isContentRoot) {
303
- claim([...remaining].find((child) => isInjectedChild(child) && isSectionFolder(child, section)));
304
- }
305
- } else {
306
- claim(matching(context, [...remaining], folderPath, entry));
268
+ continue;
269
+ }
270
+ const child = nodeNamed(context, [...remaining], folderPath, entry);
271
+ if (child !== undefined) {
272
+ remaining.delete(child);
273
+ named.push(child);
307
274
  }
308
275
  }
309
276
 
310
- // What is left keeps its default order, in three bands. The generated sections and the API
311
- // reference have defaults of their own rather than travelling with the content, so that
312
- // a later release adding a section never splits the band or reorders a sidebar nobody
313
- // touched. The sections keep the CLI's order: Fumadocs' own is by path, which would put
314
- // `context-plugin` above `sdks`.
315
- const rest = [...remaining];
316
- const api = rest.filter((child) => isApiReference(child));
317
- const injected = rest.filter(isInjectedChild).sort((left, right) => sectionRank(left) - sectionRank(right));
318
- const content = rest.filter((child) => !isApiReference(child) && !isInjectedChild(child));
319
-
320
- // With no rest token, unnamed content joins the user's own pages: after the last one the
321
- // file named. Appending at the end instead would drop it below the whole API reference,
322
- // and inserting before the reference would lift it above one the file deliberately put
323
- // first.
324
- const at = restIndex ?? afterNamedContent(named, isInjectedChild);
325
- const ordered = [...named.slice(0, at), ...content, ...named.slice(at)];
326
-
327
- // Before the API reference, but never above the user's own pages: when the file puts the
328
- // reference first, the band follows the content instead.
329
- const anchor = Math.max(anchorIn(ordered), at + content.length);
330
- return [...ordered.slice(0, anchor), ...injected, ...ordered.slice(anchor), ...api];
277
+ const at = restIndex ?? named.length;
278
+ return [...named.slice(0, at), ...remaining, ...named.slice(at)];
279
+ }
280
+
281
+ /**
282
+ * The child an entry names, or undefined. A token names a node that lives at the content root,
283
+ * and the CLI refuses each one anywhere else; honouring it in a nested directory would let the
284
+ * two halves of the format disagree about a file only one of them had rejected.
285
+ */
286
+ function nodeNamed(context: NavigationContext, children: Node[], folderPath: string, entry: string): Node | undefined {
287
+ const isContentRoot = folderPath === '';
288
+ const section = GENERATED_SECTIONS.find((candidate) => candidate.token === entry);
289
+ if (entry === API_REFERENCE_TOKEN) {
290
+ return isContentRoot ? children.find((child) => isApiReference(child)) : undefined;
291
+ }
292
+ if (section !== undefined) {
293
+ // The cheap path check first: `isInjected` reads storage.
294
+ return isContentRoot
295
+ ? children.find((child) => isSectionFolder(child, section) && isInjected(context, child))
296
+ : undefined;
297
+ }
298
+ return matching(context, children, folderPath, entry);
331
299
  }
332
300
 
333
301
  /**
@@ -346,22 +314,6 @@ function matching(context: NavigationContext, children: Node[], folderPath: stri
346
314
  return children.find((child) => child.type === 'page' && child.$ref === pagePath);
347
315
  }
348
316
 
349
- function afterNamedContent(named: Node[], isInjectedChild: (child: Node) => boolean): number {
350
- for (let index = named.length - 1; index >= 0; index -= 1) {
351
- if (!isApiReference(named[index]) && !isInjectedChild(named[index])) {
352
- return index + 1;
353
- }
354
- }
355
- // Nothing named is the user's own, so everything named is what the CLI adds, and the
356
- // user's pages keep their default place above all of it.
357
- return 0;
358
- }
359
-
360
- function anchorIn(children: Node[]): number {
361
- const index = children.findIndex((child) => isApiReference(child));
362
- return index === -1 ? children.length : index;
363
- }
364
-
365
317
  function isApiReference(child: Node): child is Folder {
366
318
  return child.type === 'folder' && child.$ref?.folder === apiBaseDir;
367
319
  }
@@ -383,15 +335,6 @@ function isInjected(context: NavigationContext, child: Node): boolean {
383
335
  return isFromSource(context, child, GENERATED_SOURCE);
384
336
  }
385
337
 
386
- /**
387
- * Where a generated folder comes in `GENERATED_SECTIONS`. Anything else sorts after them, which
388
- * only a loose generated page could be, and the CLI writes none.
389
- */
390
- function sectionRank(child: Node): number {
391
- const rank = GENERATED_SECTIONS.findIndex((section) => isSectionFolder(child, section));
392
- return rank === -1 ? GENERATED_SECTIONS.length : rank;
393
- }
394
-
395
338
  function isSectionFolder(child: Node, section: (typeof GENERATED_SECTIONS)[number]): boolean {
396
339
  return child.type === 'folder' && child.$ref?.folder === section.folder;
397
340
  }
@@ -1,11 +1,9 @@
1
- import type { useOperationContext } from 'fumadocs-openapi/ui';
1
+ import type { ParameterObject } from 'fumadocs-openapi';
2
+ import type { ExampleRequest, OperationParameters } from 'fumadocs-openapi/operation';
2
3
  import { isJsonObject } from './json';
3
4
 
4
- export type RequestExample = Pick<
5
- ReturnType<typeof useOperationContext>['examples'][number],
6
- 'id' | 'name' | 'description'
7
- >;
8
- type Location = 'path' | 'query' | 'header' | 'cookie';
5
+ export type RequestExample = Pick<ExampleRequest, 'id' | 'name' | 'description'>;
6
+ type Location = OperationParameters['in'];
9
7
  type Resolve = (node: unknown) => unknown;
10
8
 
11
9
  interface Example {
@@ -25,22 +23,16 @@ export class Parameter {
25
23
  private readonly examples: Map<string, Example>
26
24
  ) {}
27
25
 
28
- public static listIn(operation: unknown, pathItem: unknown, resolve: Resolve): Parameter[] {
29
- return [...parametersOf(operation), ...parametersOf(pathItem)].flatMap(
30
- (entry) => Parameter.from(resolve(entry), resolve) ?? []
31
- );
26
+ // Takes the parameters Fumadocs has already resolved, deduplicated and grouped by location.
27
+ public static listIn(groups: OperationParameters[], resolve: Resolve): Parameter[] {
28
+ return groups.flatMap((group) => group.items.map((item) => Parameter.from(group.in, item, resolve)));
32
29
  }
33
30
 
34
- private static from(entry: unknown, resolve: Resolve): Parameter | undefined {
35
- if (!isJsonObject(entry) || !isLocation(entry.in) || typeof entry.name !== 'string') {
36
- return undefined;
37
- }
38
- const examples = isJsonObject(entry.examples)
39
- ? Object.entries(entry.examples)
40
- .map(([id, example]): [string, unknown] => [id, resolve(example)])
41
- .filter(isExampleEntry)
42
- : [];
43
- return new Parameter(entry.in, new Map(examples));
31
+ private static from(location: Location, item: ParameterObject, resolve: Resolve): Parameter {
32
+ const examples = Object.entries(item.examples ?? {})
33
+ .map(([id, example]): [string, unknown] => [id, resolve(example)])
34
+ .filter(isExampleEntry);
35
+ return new Parameter(location, new Map(examples));
44
36
  }
45
37
 
46
38
  public namedExamples(): [string, Example][] {
@@ -58,14 +50,6 @@ export function requestExamples(bodyExamples: RequestExample[], parameters: Para
58
50
  return named.map(([id, { summary, description }]) => ({ id, name: summary || id, description }));
59
51
  }
60
52
 
61
- function parametersOf(node: unknown): unknown[] {
62
- return isJsonObject(node) && Array.isArray(node.parameters) ? node.parameters : [];
63
- }
64
-
65
- function isLocation(value: unknown): value is Location {
66
- return value === 'path' || value === 'query' || value === 'header' || value === 'cookie';
67
- }
68
-
69
53
  function isExampleEntry(entry: [string, unknown]): entry is [string, Example] {
70
54
  return isJsonObject(entry[1]) && 'value' in entry[1];
71
55
  }
@@ -3,6 +3,7 @@ import { applyMdxPreset } from 'fumadocs-mdx/config';
3
3
  import { rehypeCodeDefaultOptions } from 'fumadocs-core/mdx-plugins';
4
4
  import rehypeRaw from 'rehype-raw';
5
5
  import { rehypeCodeTitles, rehypeKeepCodeMeta } from './rehype-code-titles';
6
+ import { navigationSchema } from './navigation-schema';
6
7
 
7
8
  // What `rehype-raw` must hand on untouched, being MDX's own nodes rather than HTML.
8
9
  const MDX_NODE_TYPES = ['mdxFlowExpression', 'mdxJsxFlowElement', 'mdxJsxTextElement', 'mdxTextExpression', 'mdxjsEsm'];
@@ -18,8 +19,8 @@ export const docs = defineDocs({
18
19
  // Restricted to our own file, replacing the default of every .json and .yaml in the
19
20
  // content directory. Without it a leftover `meta.json` is loaded as a folder's metadata
20
21
  // and applied before the transformer runs -- and since a metadata file hides whatever it
21
- // does not name, the transformer could not put those pages back.
22
- meta: { files: ['**/nav.json'] }
22
+ // does not name, the transformer could not put those pages back. The schema keeps `tabs`.
23
+ meta: { files: ['**/nav.json'], schema: navigationSchema }
23
24
  });
24
25
 
25
26
  // The pages the CLI writes into this project, resolved against the Vite root like `content`.
@@ -7,10 +7,10 @@ import { containsUrl, docsRoute } from './shared';
7
7
  * transformer has run. Given to the layout rather than left to Fumadocs, which links a tab to
8
8
  * its folder's first direct page and so leaves out a tab holding only folders -- as the API
9
9
  * reference does whenever its operations are grouped. Each stays bound to its folder, which is
10
- * how the active tab is found.
10
+ * how the active tab is found. A switcher with one choice switches nothing, so one tab is none.
11
11
  */
12
12
  export function portalTabs(tree: PageTree.Root): LayoutTab[] {
13
- return tree.children.flatMap((node) => {
13
+ const tabs = tree.children.flatMap((node) => {
14
14
  if (node.type !== 'folder' || node.root !== true) return [];
15
15
  // Home keeps the order the root nav.json gives, which need not put the home page first.
16
16
  const url = containsUrl([node], docsRoute) ? docsRoute : firstPageUrl(node);
@@ -18,6 +18,7 @@ export function portalTabs(tree: PageTree.Root): LayoutTab[] {
18
18
  ? []
19
19
  : [{ title: node.name, description: node.description, icon: node.icon, url, $folder: node }];
20
20
  });
21
+ return tabs.length > 1 ? tabs : [];
21
22
  }
22
23
 
23
24
  /**
@@ -1,9 +0,0 @@
1
- import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock.core';
2
- import { useRenderContext } from 'fumadocs-openapi/ui';
3
-
4
- // Fumadocs passes `JSON.stringify(example.value)`, which is undefined for an example with no `value`.
5
- export function CodeBlock({ lang, code }: Readonly<{ lang: string; code: string | undefined }>) {
6
- const { shiki, shikiOptions } = useRenderContext();
7
- if (code === undefined) return null;
8
- return <DynamicCodeBlock lang={lang} code={code} highlighter={() => shiki.getOrInit()} options={shikiOptions} />;
9
- }