@lukoweb/apitogo 0.2.16 → 0.2.17

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.
package/dist/cli/cli.js CHANGED
@@ -7699,15 +7699,84 @@ function stableJson(value) {
7699
7699
  }
7700
7700
 
7701
7701
  // src/config/v2/wrapper.ts
7702
- import { mkdir as mkdir4, readFile as readFile5, rename as rename2, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
7702
+ import { mkdir as mkdir4, readFile as readFile6, rename as rename2, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
7703
7703
  import path12 from "node:path";
7704
7704
 
7705
+ // src/config/v2/docs-navigation.ts
7706
+ var DOCS_NAVIGATION_LABEL = "Documentation";
7707
+ var emptyBranch = () => ({ pages: /* @__PURE__ */ new Set(), folders: /* @__PURE__ */ new Map() });
7708
+ function folderLabel(name) {
7709
+ const words = name.replace(/[-_]+/g, " ").trim();
7710
+ return words.charAt(0).toUpperCase() + words.slice(1);
7711
+ }
7712
+ var byName = (a, b) => a.localeCompare(b);
7713
+ var toItems = (branch, prefix) => {
7714
+ const pages = [...branch.pages].sort(
7715
+ (a, b) => a === "index" ? -1 : b === "index" ? 1 : byName(a, b)
7716
+ );
7717
+ const folders = [...branch.folders.keys()].sort(byName);
7718
+ return [
7719
+ // The index page of a folder is the folder's own address, so the sidebar also shows on `/`.
7720
+ ...pages.map(
7721
+ (page) => page === "index" ? {
7722
+ type: "doc",
7723
+ file: `${prefix}${page}`,
7724
+ path: prefix === "" ? "/" : `/${prefix.slice(0, -1)}`
7725
+ } : `${prefix}${page}`
7726
+ ),
7727
+ ...folders.map(
7728
+ (folder) => ({
7729
+ type: "category",
7730
+ label: folderLabel(folder),
7731
+ collapsible: true,
7732
+ collapsed: false,
7733
+ items: toItems(
7734
+ branch.folders.get(folder) ?? emptyBranch(),
7735
+ `${prefix}${folder}/`
7736
+ )
7737
+ })
7738
+ )
7739
+ ];
7740
+ };
7741
+ function docsNavigation(routes) {
7742
+ const root = emptyBranch();
7743
+ for (const route of routes) {
7744
+ const parts = route.split("/").filter(Boolean);
7745
+ const page = parts.pop();
7746
+ if (!page) continue;
7747
+ const branch = parts.reduce((current, folder) => {
7748
+ const next = current.folders.get(folder) ?? emptyBranch();
7749
+ current.folders.set(folder, next);
7750
+ return next;
7751
+ }, root);
7752
+ branch.pages.add(page);
7753
+ }
7754
+ const items = toItems(root, "");
7755
+ return items.length === 0 ? [] : [{ type: "category", label: DOCS_NAVIGATION_LABEL, items }];
7756
+ }
7757
+
7705
7758
  // src/config/v2/home-redirect.ts
7706
7759
  import path11 from "node:path";
7707
7760
  import { glob as glob2 } from "glob";
7708
7761
  import globParent from "glob-parent";
7762
+
7763
+ // src/lib/util/readFrontmatter.ts
7764
+ import { readFile as readFile5 } from "node:fs/promises";
7765
+ import matter from "gray-matter";
7766
+ import { parse as parse2, stringify } from "yaml";
7767
+ var yaml = {
7768
+ parse: (input5) => parse2(input5) ?? {},
7769
+ stringify: (obj) => stringify(obj)
7770
+ };
7771
+ var readFrontmatter = async (filePath) => {
7772
+ const content = await readFile5(filePath, "utf-8");
7773
+ const normalizedContent = content.replace(/\r\n/g, "\n");
7774
+ return matter(normalizedContent, { engines: { yaml } });
7775
+ };
7776
+
7777
+ // src/config/v2/home-redirect.ts
7709
7778
  var IGNORED = ["**/node_modules/**", "**/dist/**", "**/.git/**"];
7710
- async function docRoutes(patterns) {
7779
+ async function docRoutes(patterns, options = {}) {
7711
7780
  const excluded = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
7712
7781
  const included = patterns.filter((pattern) => !pattern.startsWith("!"));
7713
7782
  const routes = await Promise.all(
@@ -7718,7 +7787,13 @@ async function docRoutes(patterns) {
7718
7787
  ignore: [...IGNORED, ...excluded]
7719
7788
  });
7720
7789
  const parent = globParent(pattern);
7721
- return files.map(
7790
+ const published = options.includeDrafts ? files : (await Promise.all(
7791
+ files.map(async (file) => ({
7792
+ file,
7793
+ draft: (await readFrontmatter(file)).data.draft === true
7794
+ }))
7795
+ )).filter(({ draft }) => !draft).map(({ file }) => file);
7796
+ return published.map(
7722
7797
  (file) => `/${path11.posix.relative(parent, file).replace(/\.mdx?$/, "")}`
7723
7798
  );
7724
7799
  })
@@ -7875,8 +7950,19 @@ function planPortal(profile, manifest) {
7875
7950
  var SITE_CONFIG_FILE = "apitogo.config.json";
7876
7951
  var REFERENCE_SPEC_FILE = "openapi/gateway.json";
7877
7952
  var API_REFERENCE_PATH = "api";
7878
- function buildSiteConfig(resolved, plan2, theme, redirects = []) {
7953
+ function buildSiteConfig(resolved, plan2, theme, extras = {}) {
7879
7954
  const portal = resolved.portal;
7955
+ const redirects = extras.redirects ?? [];
7956
+ const navigation = [
7957
+ ...extras.docsNavigation ?? [],
7958
+ ...plan2.apiReference ? [
7959
+ {
7960
+ type: "link",
7961
+ to: `/${API_REFERENCE_PATH}`,
7962
+ label: "API Reference"
7963
+ }
7964
+ ] : []
7965
+ ];
7880
7966
  return {
7881
7967
  site: {
7882
7968
  title: portal?.name ?? resolved.project.name,
@@ -7900,15 +7986,9 @@ function buildSiteConfig(resolved, plan2, theme, redirects = []) {
7900
7986
  input: `./${REFERENCE_SPEC_FILE}`,
7901
7987
  path: API_REFERENCE_PATH
7902
7988
  }
7903
- ],
7904
- navigation: [
7905
- {
7906
- type: "link",
7907
- to: `/${API_REFERENCE_PATH}`,
7908
- label: "API Reference"
7909
- }
7910
7989
  ]
7911
7990
  } : {},
7991
+ ...navigation.length > 0 ? { navigation } : {},
7912
7992
  ...redirects.length > 0 ? { redirects } : {},
7913
7993
  ...theme !== void 0 ? { theme } : {},
7914
7994
  ...plan2.auth ? { authentication: { type: "openid", ...plan2.auth } } : {},
@@ -7943,7 +8023,7 @@ async function readTheme(resolved) {
7943
8023
  const file = resolved.portal?.theme;
7944
8024
  if (!file) return void 0;
7945
8025
  try {
7946
- return JSON.parse(await readFile5(file, "utf8"));
8026
+ return JSON.parse(await readFile6(file, "utf8"));
7947
8027
  } catch {
7948
8028
  return void 0;
7949
8029
  }
@@ -8001,9 +8081,12 @@ async function preparePortal(resolved, manifest, mode, options = {}) {
8001
8081
  });
8002
8082
  }
8003
8083
  }
8084
+ const hostedDocRoutes = plan2.docs ? await docRoutes(profile.docs.content, {
8085
+ includeDrafts: mode === "development"
8086
+ }) : [];
8004
8087
  const home = homeRedirect({
8005
8088
  hostsDocs: plan2.docs,
8006
- docRoutes: plan2.docs ? await docRoutes(profile.docs.content) : [],
8089
+ docRoutes: hostedDocRoutes,
8007
8090
  externalDocsUrl: profile.docs.url,
8008
8091
  apiReferencePath: plan2.apiReference ? `/${API_REFERENCE_PATH}` : void 0,
8009
8092
  userPanel: plan2.userPanel
@@ -8012,7 +8095,10 @@ async function preparePortal(resolved, manifest, mode, options = {}) {
8012
8095
  resolved,
8013
8096
  plan2,
8014
8097
  await readTheme(resolved),
8015
- home ? [home] : []
8098
+ {
8099
+ redirects: home ? [home] : [],
8100
+ docsNavigation: docsNavigation(hostedDocRoutes)
8101
+ }
8016
8102
  );
8017
8103
  const wrapper = renderWrapper(plan2);
8018
8104
  const wrapperPath = path12.join(resolved.state.generatedDir, WRAPPER_FILE);
@@ -8057,7 +8143,7 @@ async function preparePortal(resolved, manifest, mode, options = {}) {
8057
8143
 
8058
8144
  // src/config/v2/workspace.ts
8059
8145
  import { createHash as createHash2 } from "node:crypto";
8060
- import { access as access2, lstat, readFile as readFile6, rm as rm3, symlink } from "node:fs/promises";
8146
+ import { access as access2, lstat, readFile as readFile7, rm as rm3, symlink } from "node:fs/promises";
8061
8147
  import path13 from "node:path";
8062
8148
  import { glob as glob3 } from "glob";
8063
8149
  var exists2 = (file) => access2(file).then(
@@ -8086,7 +8172,7 @@ async function materializeWorkspace(resolved, prepared) {
8086
8172
  return root;
8087
8173
  }
8088
8174
  async function configFingerprint(configPath) {
8089
- const text = await readFile6(configPath, "utf8");
8175
+ const text = await readFile7(configPath, "utf8");
8090
8176
  try {
8091
8177
  const raw = JSON.parse(text);
8092
8178
  if (raw.project) {
@@ -8135,7 +8221,7 @@ async function portalArtifactHash(resolved, manifestText, cliVersion, wrapper =
8135
8221
  for (const file of files) {
8136
8222
  hash2.update(`file:${path13.relative(resolved.configDir, file)}
8137
8223
  `);
8138
- hash2.update(await readFile6(file));
8224
+ hash2.update(await readFile7(file));
8139
8225
  }
8140
8226
  return hash2.digest("hex");
8141
8227
  }
@@ -8602,7 +8688,7 @@ async function clearProjectId(dir, projectId) {
8602
8688
 
8603
8689
  // src/config/v2/ledger.ts
8604
8690
  init_invariant();
8605
- import { mkdir as mkdir6, readFile as readFile7, rename as rename4, rm as rm5, writeFile as writeFile7 } from "node:fs/promises";
8691
+ import { mkdir as mkdir6, readFile as readFile8, rename as rename4, rm as rm5, writeFile as writeFile7 } from "node:fs/promises";
8606
8692
  import path15 from "node:path";
8607
8693
  var LEDGER_VERSION = 1;
8608
8694
  var LedgerError = class extends ApitogoError {
@@ -8619,7 +8705,7 @@ var emptyLedger = () => ({
8619
8705
  async function readLedger(state) {
8620
8706
  let text;
8621
8707
  try {
8622
- text = await readFile7(state.file, "utf8");
8708
+ text = await readFile8(state.file, "utf8");
8623
8709
  } catch (error) {
8624
8710
  if (error.code === "ENOENT")
8625
8711
  return emptyLedger();
@@ -9721,7 +9807,7 @@ import {
9721
9807
  // package.json
9722
9808
  var package_default = {
9723
9809
  name: "@lukoweb/apitogo",
9724
- version: "0.2.16",
9810
+ version: "0.2.17",
9725
9811
  type: "module",
9726
9812
  sideEffects: [
9727
9813
  "**/*.css",
@@ -11507,7 +11593,7 @@ var lookup = (schema2, path101, filePath) => {
11507
11593
  }
11508
11594
  return val;
11509
11595
  };
11510
- var stringify = (obj, indent = 2) => JSON.stringify(obj, null, indent);
11596
+ var stringify2 = (obj, indent = 2) => JSON.stringify(obj, null, indent);
11511
11597
  var generateCode = (schema2, filePath) => {
11512
11598
  const { refMap, siblingsMap } = createLocalRefMap(schema2);
11513
11599
  const lines = [];
@@ -11524,7 +11610,7 @@ var generateCode = (schema2, filePath) => {
11524
11610
  mergedRefs.set(uniqueKey, varName);
11525
11611
  lines.push(`const ${varName} = {};`);
11526
11612
  }
11527
- const toCode = (obj) => replaceMarkers(stringify(setRefMarkers(obj, refMap)), mergedRefs);
11613
+ const toCode = (obj) => replaceMarkers(stringify2(setRefMarkers(obj, refMap)), mergedRefs);
11528
11614
  const deferred = [];
11529
11615
  for (const [refPath, index] of refMap) {
11530
11616
  const value = lookup(schema2, refPath, filePath);
@@ -11535,7 +11621,7 @@ var generateCode = (schema2, filePath) => {
11535
11621
  const siblingEntry = value.__uniqueRefKey && siblingsMap.get(value.__uniqueRefKey);
11536
11622
  const target = siblingEntry ? deferred : lines;
11537
11623
  target.push(
11538
- siblingEntry ? `Object.assign(__refs[${index}], __refMap["${siblingEntry.refPath}"], ${stringify(siblingEntry.siblings)});` : `Object.assign(__refs[${index}], ${toCode(value)});`,
11624
+ siblingEntry ? `Object.assign(__refs[${index}], __refMap["${siblingEntry.refPath}"], ${stringify2(siblingEntry.siblings)});` : `Object.assign(__refs[${index}], ${toCode(value)});`,
11539
11625
  `Object.defineProperty(__refs[${index}], "__$ref", { value: __refMapPaths[${index}], enumerable: false });`
11540
11626
  );
11541
11627
  }
@@ -11544,7 +11630,7 @@ var generateCode = (schema2, filePath) => {
11544
11630
  const varName = mergedRefs.get(uniqueKey);
11545
11631
  const refIndex = refMap.get(refPath);
11546
11632
  lines.push(
11547
- `Object.assign(${varName}, __refMap["${refPath}"], ${stringify(siblings)});`,
11633
+ `Object.assign(${varName}, __refMap["${refPath}"], ${stringify2(siblings)});`,
11548
11634
  `Object.defineProperty(${varName}, "__$ref", { value: __refMapPaths[${refIndex}], enumerable: false });`
11549
11635
  );
11550
11636
  }
@@ -11806,22 +11892,6 @@ import { glob as glob4 } from "glob";
11806
11892
  import { fromMarkdown } from "mdast-util-from-markdown";
11807
11893
  import { mdxFromMarkdown } from "mdast-util-mdx";
11808
11894
  import { mdxjs } from "micromark-extension-mdxjs";
11809
-
11810
- // src/lib/util/readFrontmatter.ts
11811
- import { readFile as readFile8 } from "node:fs/promises";
11812
- import matter from "gray-matter";
11813
- import { parse as parse2, stringify as stringify2 } from "yaml";
11814
- var yaml = {
11815
- parse: (input5) => parse2(input5) ?? {},
11816
- stringify: (obj) => stringify2(obj)
11817
- };
11818
- var readFrontmatter = async (filePath) => {
11819
- const content = await readFile8(filePath, "utf-8");
11820
- const normalizedContent = content.replace(/\r\n/g, "\n");
11821
- return matter(normalizedContent, { engines: { yaml } });
11822
- };
11823
-
11824
- // src/config/validators/NavigationSchema.ts
11825
11895
  init_ZudokuConfig();
11826
11896
  var extractTitleFromContent = (content) => content.match(/^\s*#\s(.*)$/m)?.at(1);
11827
11897
  var isMdxJsxElement = (node) => node.type === "mdxJsxTextElement";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
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);