@lukoweb/apitogo 0.2.16 → 0.2.18

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.
@@ -12,6 +12,10 @@ export interface NpmInstallResult {
12
12
  durationMs: number;
13
13
  installLog: string;
14
14
  }
15
+ export declare function runNpmCommand(args: string[], cwd: string): Promise<{
16
+ durationMs: number;
17
+ log: string;
18
+ }>;
15
19
  export declare function runNpmInstall(sourceDirectory: string, options?: {
16
20
  nodeVersion?: string;
17
21
  }): Promise<NpmInstallResult>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -0,0 +1,72 @@
1
+ import type { InputNavigationItem } from "../validators/InputNavigationSchema.js";
2
+
3
+ /** Label of the one header tab that holds the sidebar made from the docs pages. */
4
+ export const DOCS_NAVIGATION_LABEL = "Documentation";
5
+
6
+ type Branch = { pages: Set<string>; folders: Map<string, Branch> };
7
+
8
+ const emptyBranch = (): Branch => ({ pages: new Set(), folders: new Map() });
9
+
10
+ /** "getting-started" becomes "Getting started". */
11
+ export function folderLabel(name: string): string {
12
+ const words = name.replace(/[-_]+/g, " ").trim();
13
+ return words.charAt(0).toUpperCase() + words.slice(1);
14
+ }
15
+
16
+ const byName = (a: string, b: string) => a.localeCompare(b);
17
+
18
+ const toItems = (branch: Branch, prefix: string): InputNavigationItem[] => {
19
+ const pages = [...branch.pages].sort((a, b) =>
20
+ a === "index" ? -1 : b === "index" ? 1 : byName(a, b),
21
+ );
22
+ const folders = [...branch.folders.keys()].sort(byName);
23
+ return [
24
+ // The index page of a folder is the folder's own address, so the sidebar also shows on `/`.
25
+ ...pages.map(
26
+ (page): InputNavigationItem =>
27
+ page === "index"
28
+ ? {
29
+ type: "doc",
30
+ file: `${prefix}${page}`,
31
+ path: prefix === "" ? "/" : `/${prefix.slice(0, -1)}`,
32
+ }
33
+ : `${prefix}${page}`,
34
+ ),
35
+ ...folders.map(
36
+ (folder): InputNavigationItem => ({
37
+ type: "category",
38
+ label: folderLabel(folder),
39
+ collapsible: true,
40
+ collapsed: false,
41
+ items: toItems(
42
+ branch.folders.get(folder) ?? emptyBranch(),
43
+ `${prefix}${folder}/`,
44
+ ),
45
+ }),
46
+ ),
47
+ ];
48
+ };
49
+
50
+ /**
51
+ * The sidebar for hosted docs, made from the pages themselves: the index page first, then the other pages by
52
+ * name, then one section per folder. `routes` are the docs plugin's route paths (`/overview`, `/guides/style`).
53
+ * Without pages there is nothing to navigate, so the result is empty.
54
+ */
55
+ export function docsNavigation(routes: string[]): InputNavigationItem[] {
56
+ const root = emptyBranch();
57
+ for (const route of routes) {
58
+ const parts = route.split("/").filter(Boolean);
59
+ const page = parts.pop();
60
+ if (!page) continue;
61
+ const branch = parts.reduce((current, folder) => {
62
+ const next = current.folders.get(folder) ?? emptyBranch();
63
+ current.folders.set(folder, next);
64
+ return next;
65
+ }, root);
66
+ branch.pages.add(page);
67
+ }
68
+ const items = toItems(root, "");
69
+ return items.length === 0
70
+ ? []
71
+ : [{ type: "category", label: DOCS_NAVIGATION_LABEL, items }];
72
+ }
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { glob } from "glob";
3
3
  import globParent from "glob-parent";
4
+ import { readFrontmatter } from "../../lib/util/readFrontmatter.js";
4
5
 
5
6
  export type HomeRedirect = { from: string; to: string };
6
7
 
@@ -10,7 +11,10 @@ const IGNORED = ["**/node_modules/**", "**/dist/**", "**/.git/**"];
10
11
  * The routes the docs plugin gives the pages these globs match: the path inside the glob's folder, without the
11
12
  * extension. Missing folders and negated globs are fine, they just match nothing.
12
13
  */
13
- export async function docRoutes(patterns: string[]): Promise<string[]> {
14
+ export async function docRoutes(
15
+ patterns: string[],
16
+ options: { includeDrafts?: boolean } = {},
17
+ ): Promise<string[]> {
14
18
  const excluded = patterns
15
19
  .filter((pattern) => pattern.startsWith("!"))
16
20
  .map((pattern) => pattern.slice(1));
@@ -24,7 +28,20 @@ export async function docRoutes(patterns: string[]): Promise<string[]> {
24
28
  ignore: [...IGNORED, ...excluded],
25
29
  });
26
30
  const parent = globParent(pattern);
27
- return files.map(
31
+ // The docs plugin leaves draft pages out of a production build, so the routes must too.
32
+ const published = options.includeDrafts
33
+ ? files
34
+ : (
35
+ await Promise.all(
36
+ files.map(async (file) => ({
37
+ file,
38
+ draft: (await readFrontmatter(file)).data.draft === true,
39
+ })),
40
+ )
41
+ )
42
+ .filter(({ draft }) => !draft)
43
+ .map(({ file }) => file);
44
+ return published.map(
28
45
  (file) =>
29
46
  `/${path.posix.relative(parent, file).replace(/\.mdx?$/, "")}`,
30
47
  );
@@ -1,5 +1,6 @@
1
1
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { docsNavigation } from "./docs-navigation.js";
3
4
  import { ConfigError, type ConfigIssue } from "./errors.js";
4
5
  import {
5
6
  ReferenceSpecError,
@@ -244,9 +245,23 @@ export function buildSiteConfig(
244
245
  resolved: ResolvedConfigV2,
245
246
  plan: PortalPlan,
246
247
  theme?: unknown,
247
- redirects: HomeRedirect[] = [],
248
+ extras: { redirects?: HomeRedirect[]; docsNavigation?: unknown[] } = {},
248
249
  ): Record<string, unknown> {
249
250
  const portal = resolved.portal;
251
+ const redirects = extras.redirects ?? [];
252
+ // One header tab for the docs sidebar, and the API reference as a link next to it.
253
+ const navigation = [
254
+ ...(extras.docsNavigation ?? []),
255
+ ...(plan.apiReference
256
+ ? [
257
+ {
258
+ type: "link",
259
+ to: `/${API_REFERENCE_PATH}`,
260
+ label: "API Reference",
261
+ },
262
+ ]
263
+ : []),
264
+ ];
250
265
  return {
251
266
  site: {
252
267
  title: portal?.name ?? resolved.project.name,
@@ -276,15 +291,9 @@ export function buildSiteConfig(
276
291
  path: API_REFERENCE_PATH,
277
292
  },
278
293
  ],
279
- navigation: [
280
- {
281
- type: "link",
282
- to: `/${API_REFERENCE_PATH}`,
283
- label: "API Reference",
284
- },
285
- ],
286
294
  }
287
295
  : {}),
296
+ ...(navigation.length > 0 ? { navigation } : {}),
288
297
  ...(redirects.length > 0 ? { redirects } : {}),
289
298
  ...(theme !== undefined ? { theme } : {}),
290
299
  ...(plan.auth ? { authentication: { type: "openid", ...plan.auth } } : {}),
@@ -433,9 +442,14 @@ export async function preparePortal(
433
442
  });
434
443
  }
435
444
  }
445
+ const hostedDocRoutes = plan.docs
446
+ ? await docRoutes(profile.docs.content, {
447
+ includeDrafts: mode === "development",
448
+ })
449
+ : [];
436
450
  const home = homeRedirect({
437
451
  hostsDocs: plan.docs,
438
- docRoutes: plan.docs ? await docRoutes(profile.docs.content) : [],
452
+ docRoutes: hostedDocRoutes,
439
453
  externalDocsUrl: profile.docs.url,
440
454
  apiReferencePath: plan.apiReference ? `/${API_REFERENCE_PATH}` : undefined,
441
455
  userPanel: plan.userPanel,
@@ -444,7 +458,10 @@ export async function preparePortal(
444
458
  resolved,
445
459
  plan,
446
460
  await readTheme(resolved),
447
- home ? [home] : [],
461
+ {
462
+ redirects: home ? [home] : [],
463
+ docsNavigation: docsNavigation(hostedDocRoutes),
464
+ },
448
465
  );
449
466
  const wrapper = renderWrapper(plan);
450
467
  const wrapperPath = path.join(resolved.state.generatedDir, WRAPPER_FILE);