@lukoweb/apitogo 0.1.54 → 0.1.56

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 (45) hide show
  1. package/dist/cli/cli.js +831 -398
  2. package/dist/declarations/config/default-landing-content.d.ts +2 -0
  3. package/dist/declarations/config/landing-manifest.d.ts +82 -0
  4. package/dist/declarations/config/local-manifest.d.ts +82 -0
  5. package/dist/declarations/config/resolve-modules.d.ts +3 -1
  6. package/dist/declarations/config/validators/ModulesSchema.d.ts +259 -0
  7. package/dist/declarations/config/validators/ZudokuConfig.d.ts +57 -0
  8. package/dist/declarations/lib/authentication/header-nav-filter.d.ts +4 -0
  9. package/dist/declarations/lib/authentication/providers/dev-portal-auth-pages.d.ts +5 -5
  10. package/dist/declarations/lib/authentication/providers/dev-portal-utils.d.ts +1 -0
  11. package/dist/declarations/lib/components/HeaderAuthActions.d.ts +3 -0
  12. package/dist/declarations/lib/components/SiteTitle.d.ts +6 -0
  13. package/dist/declarations/lib/components/ThemeSwitch.d.ts +3 -1
  14. package/dist/declarations/lib/components/TopNavigation.d.ts +1 -1
  15. package/dist/declarations/lib/core/ZudokuContext.d.ts +1 -0
  16. package/dist/flat-config.d.ts +95 -38
  17. package/package.json +1 -1
  18. package/src/app/main.css +2 -2
  19. package/src/config/apitogo-config-core.ts +13 -0
  20. package/src/config/apitogo-config-loader.browser.ts +17 -0
  21. package/src/config/build-apitogo-config.ts +13 -4
  22. package/src/config/default-landing-content.ts +154 -0
  23. package/src/config/landing-manifest.ts +11 -0
  24. package/src/config/landing-openapi-showcase.ts +115 -0
  25. package/src/config/loader.ts +17 -3
  26. package/src/config/local-manifest.ts +114 -10
  27. package/src/config/resolve-modules.ts +121 -32
  28. package/src/config/validators/ModulesSchema.ts +109 -0
  29. package/src/config/validators/ZudokuConfig.ts +1 -0
  30. package/src/lib/authentication/auth-header-nav.ts +5 -15
  31. package/src/lib/authentication/header-nav-filter.ts +36 -0
  32. package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +95 -35
  33. package/src/lib/authentication/providers/dev-portal-utils.ts +26 -7
  34. package/src/lib/authentication/providers/dev-portal.tsx +18 -8
  35. package/src/lib/components/Header.tsx +49 -39
  36. package/src/lib/components/HeaderAuthActions.tsx +36 -0
  37. package/src/lib/components/HeaderNavigation.tsx +11 -9
  38. package/src/lib/components/MobileTopNavigation.tsx +43 -24
  39. package/src/lib/components/SiteTitle.tsx +20 -0
  40. package/src/lib/components/ThemeSwitch.tsx +36 -2
  41. package/src/lib/components/TopNavigation.tsx +20 -33
  42. package/src/lib/core/ZudokuContext.ts +1 -0
  43. package/src/vite/config.ts +21 -0
  44. package/src/vite/plugin-auth.ts +4 -1
  45. package/src/vite/plugin-config.ts +39 -4
@@ -0,0 +1,3 @@
1
+ export declare const HeaderAuthActions: ({ className }: {
2
+ className?: string;
3
+ }) => import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,6 @@
1
+ type SiteTitleProps = {
2
+ title: string;
3
+ className?: string;
4
+ };
5
+ export declare const SiteTitle: ({ title, className }: SiteTitleProps) => import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -1 +1,3 @@
1
- export declare const ThemeSwitch: () => import("react/jsx-runtime").JSX.Element;
1
+ export declare const ThemeSwitch: ({ variant, }: {
2
+ variant?: "default" | "compact";
3
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { type NavLinkProps } from "react-router";
2
2
  import type { NavigationItem } from "../../config/validators/NavigationSchema.js";
3
- export declare const TopNavigation: () => import("react/jsx-runtime").JSX.Element;
3
+ export declare const TopNavigation: () => import("react/jsx-runtime").JSX.Element | null;
4
4
  export declare const TopNavLink: ({ isActive, children, ...props }: {
5
5
  isActive?: boolean;
6
6
  children: React.ReactNode;
@@ -50,6 +50,7 @@ type Site = Partial<{
50
50
  dir?: "ltr" | "rtl";
51
51
  showPoweredBy?: boolean;
52
52
  title?: string;
53
+ showTitleInHeader?: boolean;
53
54
  logo?: {
54
55
  src: {
55
56
  light: string;
@@ -64,7 +64,7 @@ export type _Schema15 = boolean
64
64
  /**
65
65
  * Gateway plan catalog for /pricing preview and publish (canonical storage).
66
66
  */
67
- export type _Schema35 = {
67
+ export type _Schema37 = {
68
68
  sku: string
69
69
  displayName: string
70
70
  priceAmount?: number
@@ -73,31 +73,31 @@ export type _Schema35 = {
73
73
  limitsJson?: string
74
74
  includedActionKeys?: string[]
75
75
  }[]
76
- export type _Schema36 = ({
76
+ export type _Schema38 = ({
77
77
  type: "url"
78
- input: (string | _Schema37[])
79
- server?: _Schema38
80
- path?: _Schema39
81
- categories?: _Schema40
82
- options?: _Schema42
78
+ input: (string | _Schema39[])
79
+ server?: _Schema40
80
+ path?: _Schema41
81
+ categories?: _Schema42
82
+ options?: _Schema44
83
83
  } | {
84
84
  type: "file"
85
- input: (string | (string | _Schema37)[])
86
- server?: _Schema38
87
- path?: _Schema39
88
- categories?: _Schema40
89
- options?: _Schema42
85
+ input: (string | (string | _Schema39)[])
86
+ server?: _Schema40
87
+ path?: _Schema41
88
+ categories?: _Schema42
89
+ options?: _Schema44
90
90
  } | {
91
91
  type: "raw"
92
92
  input: string
93
- server?: _Schema38
94
- path?: _Schema39
95
- categories?: _Schema40
96
- options?: _Schema42
93
+ server?: _Schema40
94
+ path?: _Schema41
95
+ categories?: _Schema42
96
+ options?: _Schema44
97
97
  })
98
- export type _Schema38 = string
99
- export type _Schema39 = string
100
- export type _Schema40 = {
98
+ export type _Schema40 = string
99
+ export type _Schema41 = string
100
+ export type _Schema42 = {
101
101
  label: string
102
102
  tags: string[]
103
103
  }[]
@@ -137,6 +137,7 @@ export interface FlatZudokuConfig {
137
137
  }
138
138
  site?: {
139
139
  title?: string
140
+ showTitleInHeader?: boolean
140
141
  logoUrl?: string
141
142
  dir?: ("ltr" | "rtl")
142
143
  logo?: _Schema0
@@ -395,10 +396,10 @@ export interface FlatZudokuConfig {
395
396
  modules?: {
396
397
  docs?: _Schema14
397
398
  landing?: _Schema16
398
- userPanel?: _Schema23
399
+ userPanel?: _Schema25
399
400
  }
400
- apis?: (_Schema36 | _Schema36[])
401
- catalogs?: (_Schema43 | _Schema43[])
401
+ apis?: (_Schema38 | _Schema38[])
402
+ catalogs?: (_Schema45 | _Schema45[])
402
403
  apiKeys?: {
403
404
  enabled: boolean
404
405
  getConsumers?: unknown
@@ -426,7 +427,7 @@ export interface FlatZudokuConfig {
426
427
  }
427
428
  enableStatusPages?: boolean
428
429
  defaults?: {
429
- apis: _Schema42
430
+ apis: _Schema44
430
431
  examplesLanguage?: string
431
432
  }
432
433
  __pluginDirs?: string[]
@@ -524,6 +525,10 @@ export interface _Schema16 {
524
525
  */
525
526
  layout?: ("default" | "landing" | "none")
526
527
  content?: _Schema21
528
+ /**
529
+ * When true, merge DEFAULT_LANDING_CONTENT for unset sections. When false or omitted, only configured sections are shown.
530
+ */
531
+ useDefaults?: boolean
527
532
  }
528
533
  /**
529
534
  * Default landing page content when no custom component is provided.
@@ -531,25 +536,77 @@ export interface _Schema16 {
531
536
  export interface _Schema21 {
532
537
  hero?: {
533
538
  title?: string
539
+ /**
540
+ * Accent-colored word or phrase shown on a new line after the title.
541
+ */
542
+ titleAccent?: string
534
543
  subtitle?: string
535
544
  description?: string
545
+ badge?: string
546
+ highlights?: string[]
547
+ codeExample?: {
548
+ filename?: string
549
+ language?: string
550
+ code: string
551
+ }
552
+ }
553
+ featuresSection?: {
554
+ label?: string
555
+ title?: string
556
+ description?: string
536
557
  }
537
558
  features?: {
538
559
  title: string
539
560
  description: string
561
+ icon?: string
540
562
  }[]
541
- primaryAction?: _Schema22
542
- secondaryAction?: _Schema22
563
+ primaryAction?: _Schema23
564
+ secondaryAction?: _Schema23
565
+ ticker?: {
566
+ enabled?: boolean
567
+ items?: {
568
+ label: string
569
+ value: string
570
+ change?: string
571
+ }[]
572
+ }
573
+ trust?: {
574
+ enabled?: boolean
575
+ title?: string
576
+ logos?: string[]
577
+ }
578
+ apiShowcase?: {
579
+ enabled?: boolean
580
+ title?: string
581
+ subtitle?: string
582
+ browseAllHref?: string
583
+ browseAllLabel?: string
584
+ autoFromOpenApi?: boolean
585
+ items?: {
586
+ name: string
587
+ path: string
588
+ icon?: string
589
+ latency?: string
590
+ summary?: string
591
+ }[]
592
+ }
593
+ pricingCta?: {
594
+ enabled?: boolean
595
+ title?: string
596
+ description?: string
597
+ primaryAction?: _Schema23
598
+ secondaryAction?: _Schema23
599
+ }
543
600
  showPoweredBy?: boolean
544
601
  }
545
- export interface _Schema22 {
602
+ export interface _Schema23 {
546
603
  label: string
547
604
  href: string
548
605
  }
549
606
  /**
550
607
  * User panel module — authenticated user area with sub-modules.
551
608
  */
552
- export interface _Schema23 {
609
+ export interface _Schema25 {
553
610
  enabled?: _Schema15
554
611
  /**
555
612
  * Base path prefix for user panel routes.
@@ -571,7 +628,7 @@ export interface _Schema23 {
571
628
  component?: {
572
629
  [k: string]: unknown
573
630
  }
574
- content?: _Schema30
631
+ content?: _Schema32
575
632
  }
576
633
  userManagement?: {
577
634
  enabled?: _Schema15
@@ -591,7 +648,7 @@ export interface _Schema23 {
591
648
  component?: {
592
649
  [k: string]: unknown
593
650
  }
594
- content?: _Schema31
651
+ content?: _Schema33
595
652
  }
596
653
  apiKeys?: {
597
654
  enabled?: _Schema15
@@ -616,30 +673,30 @@ export interface _Schema23 {
616
673
  component?: {
617
674
  [k: string]: unknown
618
675
  }
619
- content?: _Schema34
620
- items?: _Schema35
676
+ content?: _Schema36
677
+ items?: _Schema37
621
678
  }
622
679
  }
623
680
  /**
624
681
  * Default dashboard page content when no custom component is provided.
625
682
  */
626
- export interface _Schema30 {
683
+ export interface _Schema32 {
627
684
  title?: string
628
685
  subtitle?: string
629
686
  description?: string
630
- quickLinks?: _Schema22[]
687
+ quickLinks?: _Schema23[]
631
688
  }
632
689
  /**
633
690
  * Default profile page content when no custom component is provided.
634
691
  */
635
- export interface _Schema31 {
692
+ export interface _Schema33 {
636
693
  title?: string
637
694
  description?: string
638
695
  }
639
696
  /**
640
697
  * Default plans page content when no custom component is provided.
641
698
  */
642
- export interface _Schema34 {
699
+ export interface _Schema36 {
643
700
  title?: string
644
701
  description?: string
645
702
  tiers?: {
@@ -648,12 +705,12 @@ export interface _Schema34 {
648
705
  description?: string
649
706
  }[]
650
707
  }
651
- export interface _Schema37 {
708
+ export interface _Schema39 {
652
709
  path: string
653
710
  input: string
654
711
  label?: string
655
712
  }
656
- export interface _Schema42 {
713
+ export interface _Schema44 {
657
714
  examplesLanguage?: string
658
715
  supportedLanguages?: {
659
716
  value: string
@@ -670,7 +727,7 @@ export interface _Schema42 {
670
727
  transformExamples?: unknown
671
728
  generateCodeSnippet?: unknown
672
729
  }
673
- export interface _Schema43 {
730
+ export interface _Schema45 {
674
731
  path: string
675
732
  label: string
676
733
  items?: string[]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
package/src/app/main.css CHANGED
@@ -102,8 +102,8 @@
102
102
 
103
103
  @layer base {
104
104
  :root {
105
- --top-header-height: 65px;
106
- --top-nav-height: 50px;
105
+ --top-header-height: 64px;
106
+ --top-nav-height: 0px;
107
107
  --banner-height: 35px;
108
108
  --header-height: calc(
109
109
  var(--top-header-height) + var(--top-nav-height) + var(--banner-height)
@@ -12,6 +12,7 @@ const DERIVED_MANIFEST_KEYS = [
12
12
  "backend",
13
13
  "gateway",
14
14
  "projectId",
15
+ "landing",
15
16
  ] as const;
16
17
 
17
18
  const SITE_CONFIG_KEYS = [
@@ -542,6 +543,18 @@ export function deriveManifestFields(
542
543
  }
543
544
  continue;
544
545
  }
546
+ if (key === "landing") {
547
+ const modules = asRecord(normalized.modules);
548
+ const landing = asRecord(modules?.landing);
549
+ if (landing?.content || landing?.enabled !== undefined) {
550
+ manifest.landing = {
551
+ enabled:
552
+ typeof landing.enabled === "boolean" ? landing.enabled : undefined,
553
+ content: landing.content,
554
+ };
555
+ }
556
+ continue;
557
+ }
545
558
  if (normalized[key] !== undefined) {
546
559
  manifest[key] = normalized[key];
547
560
  }
@@ -0,0 +1,17 @@
1
+ import type {
2
+ ApitogoJsonConfig,
3
+ AuthenticationSecrets,
4
+ } from "./apitogo-config-core.js";
5
+
6
+ /** Browser stub — JSON config is embedded by the Vite config plugin on the client. */
7
+ export function loadApitogoConfig(_rootDir?: string): ApitogoJsonConfig {
8
+ return {};
9
+ }
10
+
11
+ export function readAuthenticationSecrets(): AuthenticationSecrets {
12
+ return {};
13
+ }
14
+
15
+ export function apitogoConfigPath(rootDir: string): string {
16
+ return rootDir;
17
+ }
@@ -11,6 +11,13 @@ type BuildApitogoConfigOptions = Partial<ApitogoConfig> &
11
11
  plugins?: ApitogoPlugin[];
12
12
  };
13
13
 
14
+ function mergeOptionalRecord<T extends Record<string, unknown>>(
15
+ ...records: Array<T | undefined>
16
+ ): T | undefined {
17
+ const merged = Object.assign({}, ...records.filter(Boolean)) as T;
18
+ return Object.keys(merged).length > 0 ? merged : undefined;
19
+ }
20
+
14
21
  export function buildApitogoConfig({
15
22
  plugins,
16
23
  plans: _legacyPlans,
@@ -24,14 +31,16 @@ export function buildApitogoConfig({
24
31
  const { plans: _rootPlans, ...restOverrides } =
25
32
  jsonOverrides as ApitogoJsonConfig;
26
33
 
34
+ const authentication = mergeOptionalRecord(
35
+ siteConfig.authentication as ApitogoConfig["authentication"],
36
+ jsonOverrides.authentication as ApitogoConfig["authentication"],
37
+ );
38
+
27
39
  return {
28
40
  ...siteConfig,
29
41
  ...restOverrides,
30
42
  plugins,
31
- authentication: {
32
- ...(siteConfig.authentication as ApitogoConfig["authentication"]),
33
- ...jsonOverrides.authentication,
34
- },
43
+ ...(authentication ? { authentication } : {}),
35
44
  site: {
36
45
  ...(siteConfig.site as ApitogoConfig["site"]),
37
46
  ...jsonOverrides.site,
@@ -0,0 +1,154 @@
1
+ import type { ResolvedLandingContent } from "./validators/ModulesSchema.js";
2
+
3
+ /** Default landing sections aligned with webapp-template/APIToGo.dc.html */
4
+ export const DEFAULT_LANDING_CONTENT: ResolvedLandingContent = {
5
+ hero: {
6
+ badge: "99.98% uptime",
7
+ subtitle: "Production-ready APIs, one endpoint away",
8
+ title: "Developer APIs that ship",
9
+ titleAccent: "today.",
10
+ description:
11
+ "Documentation, interactive API reference, auth, and billing — sub-40ms responses, one API key, generous free tier.",
12
+ highlights: ["No credit card", "10k requests free", "SDKs for 6 languages"],
13
+ codeExample: {
14
+ filename: "example.sh",
15
+ language: "bash",
16
+ code: `$ curl https://api.example.com/v1/items \\
17
+ -H "Authorization: Bearer YOUR_API_KEY" \\
18
+ -d "limit=10"
19
+
20
+ {
21
+ "items": [
22
+ { "id": "item_1", "name": "Widget", "status": "active" },
23
+ { "id": "item_2", "name": "Gadget", "status": "active" }
24
+ ]
25
+ } // 28ms`,
26
+ },
27
+ },
28
+ primaryAction: { label: "Get your API key", href: "/register" },
29
+ secondaryAction: { label: "Read the docs", href: "/introduction" },
30
+ ticker: {
31
+ items: [
32
+ { label: "GET", value: "/v1/items", change: "28ms" },
33
+ { label: "POST", value: "/v1/items", change: "34ms" },
34
+ { label: "GET", value: "/v1/users", change: "19ms" },
35
+ { label: "PATCH", value: "/v1/users/{id}", change: "41ms" },
36
+ { label: "GET", value: "/v1/health", change: "16ms" },
37
+ { label: "DELETE", value: "/v1/items/{id}", change: "22ms" },
38
+ { label: "GET", value: "/v1/search", change: "47ms" },
39
+ { label: "POST", value: "/v1/auth/token", change: "52ms" },
40
+ ],
41
+ },
42
+ trust: {
43
+ title: "Trusted by teams shipping production APIs",
44
+ logos: ["Acme Corp", "BuildFast", "DataFlow", "ShipKit", "DevStack", "LaunchPad"],
45
+ },
46
+ featuresSection: {
47
+ label: "// why us",
48
+ title: "Everything you need to build on your API",
49
+ description:
50
+ "Skip the infra. We handle docs, auth, keys, and billing so you can focus on your product.",
51
+ },
52
+ features: [
53
+ {
54
+ icon: "gauge",
55
+ title: "Sub-40ms responses",
56
+ description:
57
+ "Edge-cached and globally distributed, so your app feels instant anywhere.",
58
+ },
59
+ {
60
+ icon: "radio",
61
+ title: "Real-time streams",
62
+ description:
63
+ "WebSocket feeds with automatic reconnection for live data and events.",
64
+ },
65
+ {
66
+ icon: "blocks",
67
+ title: "Unified schema",
68
+ description:
69
+ "One consistent API design — no per-endpoint glue code or custom adapters.",
70
+ },
71
+ {
72
+ icon: "shield-check",
73
+ title: "99.98% uptime",
74
+ description:
75
+ "Redundant infra with a public status page and an SLA on paid plans.",
76
+ },
77
+ {
78
+ icon: "code",
79
+ title: "SDKs for 6 languages",
80
+ description:
81
+ "Typed clients for JS, Python, Go, Rust, Ruby and PHP. Copy, paste, ship.",
82
+ },
83
+ {
84
+ icon: "webhook",
85
+ title: "Webhooks & alerts",
86
+ description:
87
+ "Push events and threshold alerts straight to your endpoints.",
88
+ },
89
+ ],
90
+ apiShowcase: {
91
+ title: "Production APIs. One key.",
92
+ browseAllLabel: "Browse all APIs",
93
+ browseAllHref: "/api",
94
+ autoFromOpenApi: true,
95
+ items: [
96
+ {
97
+ name: "List resources",
98
+ path: "/v1/items",
99
+ icon: "boxes",
100
+ latency: "28ms",
101
+ },
102
+ {
103
+ name: "Create resource",
104
+ path: "/v1/items",
105
+ icon: "badge-dollar-sign",
106
+ latency: "34ms",
107
+ },
108
+ {
109
+ name: "Get resource",
110
+ path: "/v1/items/{id}",
111
+ icon: "layers",
112
+ latency: "19ms",
113
+ },
114
+ {
115
+ name: "Search",
116
+ path: "/v1/search",
117
+ icon: "arrow-left-right",
118
+ latency: "41ms",
119
+ },
120
+ {
121
+ name: "User profile",
122
+ path: "/v1/users/me",
123
+ icon: "wallet",
124
+ latency: "52ms",
125
+ },
126
+ {
127
+ name: "Health check",
128
+ path: "/v1/health",
129
+ icon: "activity",
130
+ latency: "16ms",
131
+ },
132
+ {
133
+ name: "Metadata",
134
+ path: "/v1/meta",
135
+ icon: "coins",
136
+ latency: "22ms",
137
+ },
138
+ {
139
+ name: "Rate limits",
140
+ path: "/v1/limits",
141
+ icon: "gauge",
142
+ latency: "16ms",
143
+ },
144
+ ],
145
+ },
146
+ pricingCta: {
147
+ title: "Start free. Scale when you do.",
148
+ description:
149
+ "10,000 requests a month, free forever. Upgrade only when your app takes off.",
150
+ primaryAction: { label: "Create free account", href: "/register" },
151
+ secondaryAction: { label: "See pricing", href: "/pricing" },
152
+ },
153
+ showPoweredBy: true,
154
+ };
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+ import { LandingContentSchema } from "./validators/ModulesSchema.js";
3
+
4
+ export const LandingManifestContentSchema = LandingContentSchema;
5
+
6
+ export type LandingManifestContent = z.infer<typeof LandingContentSchema>;
7
+
8
+ export type LandingManifestInput = {
9
+ enabled?: boolean;
10
+ content?: LandingManifestContent;
11
+ };
@@ -0,0 +1,115 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import type { LandingApiShowcaseItemConfig } from "./validators/ModulesSchema.js";
4
+ import type { ZudokuConfig } from "./validators/ZudokuConfig.js";
5
+
6
+ const HTTP_METHODS = [
7
+ "get",
8
+ "post",
9
+ "put",
10
+ "patch",
11
+ "delete",
12
+ "head",
13
+ "options",
14
+ ] as const;
15
+
16
+ type OpenApiDocument = {
17
+ paths?: Record<
18
+ string,
19
+ Partial<Record<(typeof HTTP_METHODS)[number], { summary?: string }>>
20
+ >;
21
+ };
22
+
23
+ function readOpenApiDocument(
24
+ rootDir: string,
25
+ openApiPath: string,
26
+ ): OpenApiDocument | null {
27
+ try {
28
+ const absolutePath = path.isAbsolute(openApiPath)
29
+ ? openApiPath
30
+ : path.resolve(rootDir, openApiPath);
31
+ const raw = readFileSync(absolutePath, "utf8");
32
+ return JSON.parse(raw) as OpenApiDocument;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function resolveOpenApiPath(config: ZudokuConfig): string | undefined {
39
+ const backend = (config as { backend?: { openApiPath?: string } }).backend;
40
+ if (backend?.openApiPath) {
41
+ return backend.openApiPath;
42
+ }
43
+
44
+ const apis = config.apis;
45
+ if (!apis) {
46
+ return undefined;
47
+ }
48
+
49
+ const entries = Array.isArray(apis) ? apis : [apis];
50
+ for (const entry of entries) {
51
+ if (typeof entry === "object" && entry !== null && "type" in entry) {
52
+ const api = entry as { type?: string; input?: string };
53
+ if (api.type === "file" && typeof api.input === "string") {
54
+ return api.input;
55
+ }
56
+ }
57
+ }
58
+
59
+ return undefined;
60
+ }
61
+
62
+ export function extractApiShowcaseItemsFromOpenApi(
63
+ config: ZudokuConfig,
64
+ rootDir: string,
65
+ limit = 8,
66
+ ): LandingApiShowcaseItemConfig[] {
67
+ const openApiPath = resolveOpenApiPath(config);
68
+ if (!openApiPath) {
69
+ return [];
70
+ }
71
+
72
+ const document = readOpenApiDocument(rootDir, openApiPath);
73
+ if (!document?.paths) {
74
+ return [];
75
+ }
76
+
77
+ const items: LandingApiShowcaseItemConfig[] = [];
78
+
79
+ for (const [routePath, methods] of Object.entries(document.paths)) {
80
+ for (const method of HTTP_METHODS) {
81
+ const operation = methods?.[method];
82
+ if (!operation) {
83
+ continue;
84
+ }
85
+
86
+ items.push({
87
+ name: operation.summary ?? `${method.toUpperCase()} ${routePath}`,
88
+ path: routePath,
89
+ summary: operation.summary,
90
+ });
91
+
92
+ if (items.length >= limit) {
93
+ return items;
94
+ }
95
+ }
96
+ }
97
+
98
+ return items;
99
+ }
100
+
101
+ export function enrichResolvedLandingApiShowcase(
102
+ config: ZudokuConfig,
103
+ resolved: { landing: { content: { apiShowcase?: { autoFromOpenApi?: boolean; items?: LandingApiShowcaseItemConfig[] } } } },
104
+ rootDir: string,
105
+ ): void {
106
+ const showcase = resolved.landing.content.apiShowcase;
107
+ if (
108
+ !showcase?.autoFromOpenApi ||
109
+ (showcase.items && showcase.items.length > 0)
110
+ ) {
111
+ return;
112
+ }
113
+
114
+ showcase.items = extractApiShowcaseItemsFromOpenApi(config, rootDir);
115
+ }