@lukoweb/apitogo 0.2.49 → 0.2.51
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 +846 -655
- package/dist/declarations/lib/plugins/openapi/util/createHttpSnippet.d.ts +2 -0
- package/docs/cli/shape.md +7 -7
- package/docs/configuration/make-config.mdx +13 -10
- package/docs/configuration/navigation.mdx +1 -1
- package/docs/guides/custom-pages.md +33 -4
- package/package.json +1 -1
- package/src/config/v2/docs-navigation.ts +48 -25
- package/src/config/v2/full-server.ts +1 -1
- package/src/config/v2/gateway-reference.ts +15 -2
- package/src/config/v2/index.ts +1 -0
- package/src/config/v2/init.ts +1 -1
- package/src/config/v2/rules.ts +4 -1
- package/src/config/v2/starter-landing.ts +13 -6
- package/src/config/v2/tsx-pages.ts +100 -0
- package/src/config/v2/workspace.ts +5 -0
- package/src/config/v2/wrapper.ts +96 -9
- package/src/lib/plugins/openapi/util/createHttpSnippet.ts +147 -135
package/src/config/v2/wrapper.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { type PortalProfile, portalProfile } from "./portal-profile.js";
|
|
24
24
|
import { protectedRoutesFor } from "./protected-routes.js";
|
|
25
25
|
import type { ResolvedConfigV2 } from "./resolve.js";
|
|
26
|
+
import { findTsxPages, TSX_PAGE_ELEMENT_PREFIX } from "./tsx-pages.js";
|
|
26
27
|
|
|
27
28
|
export const WRAPPER_FILE = "apitogo.config.tsx";
|
|
28
29
|
|
|
@@ -76,6 +77,10 @@ export type PortalPlan = {
|
|
|
76
77
|
billing?: string;
|
|
77
78
|
/** Where the account pages link to: only places this portal has (hosted docs, an external docs site, the API page). */
|
|
78
79
|
links?: { docs?: string; api?: string };
|
|
80
|
+
/** The header API users send their key in, from the gateway entry of services.json (API-120). */
|
|
81
|
+
keyHeader?: string;
|
|
82
|
+
/** The gateway's address, for code samples on the portal's pages. */
|
|
83
|
+
gatewayBaseUrl?: string;
|
|
79
84
|
};
|
|
80
85
|
/** Account pages. */
|
|
81
86
|
userPanel: boolean;
|
|
@@ -258,6 +263,10 @@ export function planPortal(
|
|
|
258
263
|
apiBaseUrl: plans.apiBaseUrl,
|
|
259
264
|
keysApiBaseUrl: plans.keysApiBaseUrl,
|
|
260
265
|
billing: plans.billing,
|
|
266
|
+
...(gateway?.keyHeaderName ? { keyHeader: gateway.keyHeaderName } : {}),
|
|
267
|
+
...(gateway?.baseUrl && serviceReady(gateway)
|
|
268
|
+
? { gatewayBaseUrl: gateway.baseUrl }
|
|
269
|
+
: {}),
|
|
261
270
|
links: {
|
|
262
271
|
// Hosted docs are linked once their pages are known (see `buildPortalWorkspace`): there is no page to
|
|
263
272
|
// name before that, and a guessed one was a 404 on every portal whose first page is not called `introduction`.
|
|
@@ -424,8 +433,17 @@ function authenticationFor(plan: PortalPlan): Record<string, unknown> {
|
|
|
424
433
|
export const PREVIEW_BANNER_MESSAGE =
|
|
425
434
|
"Preview mode: sign in with demo@apitogo.local / password to try the console. Run `apitogo deploy` for the real services.";
|
|
426
435
|
|
|
427
|
-
/**
|
|
428
|
-
export
|
|
436
|
+
/** A React docs page the wrapper imports: `importPath` is relative to the wrapper, through the `content` link. */
|
|
437
|
+
export type WrapperPage = { id: string; importPath: string };
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* The wrapper source. Imports exist only for what the plan registers. React docs pages are imported here and put in
|
|
441
|
+
* place of their sidebar placeholders, since the site config is JSON and cannot hold a component.
|
|
442
|
+
*/
|
|
443
|
+
export function renderWrapper(
|
|
444
|
+
plan: PortalPlan,
|
|
445
|
+
pages: readonly WrapperPage[] = [],
|
|
446
|
+
): string {
|
|
429
447
|
const imports = [
|
|
430
448
|
'import { buildApitogoConfig } from "@lukoweb/apitogo/build-config";',
|
|
431
449
|
...(plan.plansPlugin
|
|
@@ -433,12 +451,41 @@ export function renderWrapper(plan: PortalPlan): string {
|
|
|
433
451
|
'import { devPortalBillingPlugin } from "@lukoweb/apitogo-plugin-dev-portal-billing";',
|
|
434
452
|
]
|
|
435
453
|
: []),
|
|
454
|
+
...(pages.length > 0 ? ['import { createElement } from "react";'] : []),
|
|
455
|
+
...pages.map(
|
|
456
|
+
(page, index) =>
|
|
457
|
+
`import TsxPage${index} from ${JSON.stringify(page.importPath)};`,
|
|
458
|
+
),
|
|
436
459
|
`import siteConfig from "./${SITE_CONFIG_FILE}";`,
|
|
437
460
|
];
|
|
438
461
|
const plugins = plan.plansPlugin
|
|
439
462
|
? `[devPortalBillingPlugin(${JSON.stringify(plan.plansPlugin)})]`
|
|
440
463
|
: "[]";
|
|
441
464
|
|
|
465
|
+
const pageLines =
|
|
466
|
+
pages.length === 0
|
|
467
|
+
? []
|
|
468
|
+
: [
|
|
469
|
+
"",
|
|
470
|
+
"// React docs pages: each sidebar placeholder becomes the page's component.",
|
|
471
|
+
"const tsxPages: Record<string, any> = {",
|
|
472
|
+
...pages.map(
|
|
473
|
+
(page, index) => ` ${JSON.stringify(page.id)}: TsxPage${index},`,
|
|
474
|
+
),
|
|
475
|
+
"};",
|
|
476
|
+
"const withTsxPages = (items: any[]): any[] =>",
|
|
477
|
+
" items.map((item) => {",
|
|
478
|
+
' if (item === null || typeof item !== "object") return item;',
|
|
479
|
+
" const page =",
|
|
480
|
+
' typeof item.element === "string" ? tsxPages[item.element] : undefined;',
|
|
481
|
+
" return {",
|
|
482
|
+
" ...item,",
|
|
483
|
+
" ...(page ? { element: createElement(page) } : {}),",
|
|
484
|
+
" ...(Array.isArray(item.items) ? { items: withTsxPages(item.items) } : {}),",
|
|
485
|
+
" };",
|
|
486
|
+
" });",
|
|
487
|
+
];
|
|
488
|
+
|
|
442
489
|
return [
|
|
443
490
|
"// Generated by apitogo from apitogo.config.json and services.json. Do not edit: it is rewritten on every build.",
|
|
444
491
|
...(plan.preview
|
|
@@ -447,11 +494,28 @@ export function renderWrapper(plan: PortalPlan): string {
|
|
|
447
494
|
]
|
|
448
495
|
: []),
|
|
449
496
|
...imports,
|
|
497
|
+
...pageLines,
|
|
450
498
|
"",
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
499
|
+
// The components go in after buildApitogoConfig: it copies the config with structuredClone, which cannot copy a
|
|
500
|
+
// React element.
|
|
501
|
+
...(pages.length > 0
|
|
502
|
+
? [
|
|
503
|
+
"const config = buildApitogoConfig({",
|
|
504
|
+
" ...siteConfig,",
|
|
505
|
+
` plugins: ${plugins},`,
|
|
506
|
+
"});",
|
|
507
|
+
"",
|
|
508
|
+
"export default {",
|
|
509
|
+
" ...config,",
|
|
510
|
+
" navigation: withTsxPages((config as any).navigation ?? []),",
|
|
511
|
+
"};",
|
|
512
|
+
]
|
|
513
|
+
: [
|
|
514
|
+
"export default buildApitogoConfig({",
|
|
515
|
+
" ...siteConfig,",
|
|
516
|
+
` plugins: ${plugins},`,
|
|
517
|
+
"});",
|
|
518
|
+
]),
|
|
455
519
|
"",
|
|
456
520
|
].join("\n");
|
|
457
521
|
}
|
|
@@ -580,11 +644,27 @@ export async function preparePortal(
|
|
|
580
644
|
});
|
|
581
645
|
}
|
|
582
646
|
}
|
|
583
|
-
const
|
|
647
|
+
const markdownRoutes = plan.docs
|
|
584
648
|
? await docRoutes(profile.docs.content, {
|
|
585
649
|
includeDrafts: mode === "development",
|
|
586
650
|
})
|
|
587
651
|
: [];
|
|
652
|
+
// React pages next to the Markdown ones. A Markdown page wins over a React page at the same address.
|
|
653
|
+
const tsxPages = plan.docs
|
|
654
|
+
? (await findTsxPages(profile.docs.content)).filter(
|
|
655
|
+
(page) => !markdownRoutes.includes(page.route),
|
|
656
|
+
)
|
|
657
|
+
: [];
|
|
658
|
+
const wrapperPages: WrapperPage[] = tsxPages.map((page, index) => ({
|
|
659
|
+
id: `${TSX_PAGE_ELEMENT_PREFIX}${index}`,
|
|
660
|
+
importPath: `./${CONTENT_LINK}/${path
|
|
661
|
+
.relative(resolved.configDir, page.file)
|
|
662
|
+
.split(path.sep)
|
|
663
|
+
.join("/")}`,
|
|
664
|
+
}));
|
|
665
|
+
const hostedDocRoutes = [
|
|
666
|
+
...new Set([...markdownRoutes, ...tsxPages.map((page) => page.route)]),
|
|
667
|
+
].sort();
|
|
588
668
|
// The account pages' "Documentation" link goes to a page the portal really has, or is left out.
|
|
589
669
|
if (plan.plansPlugin && plan.docs) {
|
|
590
670
|
const landing = hostedDocRoutes.some(
|
|
@@ -617,7 +697,14 @@ export async function preparePortal(
|
|
|
617
697
|
await readTheme(resolved),
|
|
618
698
|
{
|
|
619
699
|
redirects: home ? [home] : [],
|
|
620
|
-
docsNavigation: docsNavigation(
|
|
700
|
+
docsNavigation: docsNavigation(
|
|
701
|
+
markdownRoutes,
|
|
702
|
+
tsxPages.map((page, index) => ({
|
|
703
|
+
route: page.route,
|
|
704
|
+
label: page.label,
|
|
705
|
+
element: wrapperPages[index]?.id ?? "",
|
|
706
|
+
})),
|
|
707
|
+
),
|
|
621
708
|
docsPath: pickLandingRoute(hostedDocRoutes),
|
|
622
709
|
demoPlans:
|
|
623
710
|
plan.demo && plan.plansPlugin
|
|
@@ -633,7 +720,7 @@ export async function preparePortal(
|
|
|
633
720
|
}),
|
|
634
721
|
},
|
|
635
722
|
);
|
|
636
|
-
const wrapper = renderWrapper(plan);
|
|
723
|
+
const wrapper = renderWrapper(plan, wrapperPages);
|
|
637
724
|
const wrapperPath = path.join(resolved.state.generatedDir, WRAPPER_FILE);
|
|
638
725
|
if (options.write !== false) {
|
|
639
726
|
await mkdir(path.dirname(wrapperPath), { recursive: true });
|
|
@@ -1,135 +1,147 @@
|
|
|
1
|
-
import { HTTPSnippet } from "@zudoku/httpsnippet";
|
|
2
|
-
import type { OperationsFragmentFragment } from "../graphql/graphql.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
case "
|
|
109
|
-
converted = snippet.convert("
|
|
110
|
-
break;
|
|
111
|
-
case "
|
|
112
|
-
converted = snippet.convert("
|
|
113
|
-
break;
|
|
114
|
-
case "
|
|
115
|
-
converted = snippet.convert("
|
|
116
|
-
break;
|
|
117
|
-
case "
|
|
118
|
-
converted = snippet.convert("
|
|
119
|
-
break;
|
|
120
|
-
case "
|
|
121
|
-
converted = snippet.convert("
|
|
122
|
-
break;
|
|
123
|
-
case "
|
|
124
|
-
converted = snippet.convert("
|
|
125
|
-
break;
|
|
126
|
-
case "
|
|
127
|
-
converted = snippet.convert("
|
|
128
|
-
break;
|
|
129
|
-
|
|
130
|
-
converted = snippet.convert("
|
|
131
|
-
break;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
1
|
+
import { HTTPSnippet } from "@zudoku/httpsnippet";
|
|
2
|
+
import type { OperationsFragmentFragment } from "../graphql/graphql.js";
|
|
3
|
+
|
|
4
|
+
/** Operation extension with the header a key goes in (same name as `KEY_HEADER_EXTENSION` in the CLI's config). */
|
|
5
|
+
export const KEY_HEADER_EXTENSION = "x-apitogo-key-header";
|
|
6
|
+
|
|
7
|
+
/** What a code sample shows where the visitor's key goes. */
|
|
8
|
+
export const KEY_PLACEHOLDER = "YOUR_API_KEY";
|
|
9
|
+
|
|
10
|
+
const toFormDataParams = (text?: string) => {
|
|
11
|
+
const stringify = (v: unknown) =>
|
|
12
|
+
typeof v === "string" ? v : JSON.stringify(v);
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const obj = text && JSON.parse(text);
|
|
16
|
+
if (typeof obj !== "object" || !obj) return [];
|
|
17
|
+
|
|
18
|
+
return Object.entries(obj).flatMap(([name, value]) => {
|
|
19
|
+
const values = Array.isArray(value) ? value : [value];
|
|
20
|
+
return values.map((v) => ({ name, value: stringify(v) }));
|
|
21
|
+
});
|
|
22
|
+
} catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const createHttpSnippet = ({
|
|
28
|
+
operation,
|
|
29
|
+
selectedServer,
|
|
30
|
+
exampleBody,
|
|
31
|
+
}: {
|
|
32
|
+
operation: OperationsFragmentFragment;
|
|
33
|
+
selectedServer: string;
|
|
34
|
+
exampleBody: {
|
|
35
|
+
mimeType: string;
|
|
36
|
+
text?: string;
|
|
37
|
+
};
|
|
38
|
+
}) => {
|
|
39
|
+
const isMultipart =
|
|
40
|
+
exampleBody.mimeType === "multipart/form-data" ||
|
|
41
|
+
exampleBody.mimeType === "application/x-www-form-urlencoded";
|
|
42
|
+
|
|
43
|
+
const postData = isMultipart
|
|
44
|
+
? {
|
|
45
|
+
mimeType: exampleBody.mimeType,
|
|
46
|
+
params: toFormDataParams(exampleBody.text),
|
|
47
|
+
}
|
|
48
|
+
: exampleBody;
|
|
49
|
+
|
|
50
|
+
const keyHeader = operation.extensions?.[KEY_HEADER_EXTENSION];
|
|
51
|
+
|
|
52
|
+
return new HTTPSnippet({
|
|
53
|
+
method: operation.method.toUpperCase(),
|
|
54
|
+
url:
|
|
55
|
+
selectedServer + operation.path.replaceAll("{", ":").replaceAll("}", ""),
|
|
56
|
+
postData,
|
|
57
|
+
headers: [
|
|
58
|
+
// The gateway's key header, on operations that need a key (set by the portal build, API-120).
|
|
59
|
+
...(typeof keyHeader === "string" && keyHeader
|
|
60
|
+
? [{ name: keyHeader, value: KEY_PLACEHOLDER }]
|
|
61
|
+
: []),
|
|
62
|
+
...(exampleBody.text
|
|
63
|
+
? [{ name: "Content-Type", value: exampleBody.mimeType }]
|
|
64
|
+
: []),
|
|
65
|
+
...(operation.parameters
|
|
66
|
+
?.filter((p) => p.in === "header" && p.required === true)
|
|
67
|
+
.map((p) => ({
|
|
68
|
+
name: p.name,
|
|
69
|
+
value:
|
|
70
|
+
p.schema?.default ??
|
|
71
|
+
p.examples?.find((x) => x.value)?.value ??
|
|
72
|
+
(p.schema?.type === "string"
|
|
73
|
+
? "<string>"
|
|
74
|
+
: p.schema?.type === "number" || p.schema?.type === "integer"
|
|
75
|
+
? "<number>"
|
|
76
|
+
: p.schema?.type === "boolean"
|
|
77
|
+
? "<bool>"
|
|
78
|
+
: "<value>"),
|
|
79
|
+
})) ?? []),
|
|
80
|
+
],
|
|
81
|
+
queryString:
|
|
82
|
+
operation.parameters
|
|
83
|
+
?.filter((p) => p.in === "query" && p.required === true)
|
|
84
|
+
.map((p) => ({
|
|
85
|
+
name: p.name,
|
|
86
|
+
value:
|
|
87
|
+
p.schema?.default ??
|
|
88
|
+
p.examples?.find((x) => x.value)?.value ??
|
|
89
|
+
(p.schema?.type === "string"
|
|
90
|
+
? "<string>"
|
|
91
|
+
: p.schema?.type === "number" || p.schema?.type === "integer"
|
|
92
|
+
? "<number>"
|
|
93
|
+
: p.schema?.type === "boolean"
|
|
94
|
+
? "<bool>"
|
|
95
|
+
: "<value>"),
|
|
96
|
+
})) ?? [],
|
|
97
|
+
httpVersion: "",
|
|
98
|
+
cookies: [],
|
|
99
|
+
headersSize: 0,
|
|
100
|
+
bodySize: 0,
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const getConverted = (snippet: HTTPSnippet, option: string) => {
|
|
105
|
+
// biome-ignore lint/suspicious/noExplicitAny: Allow any type
|
|
106
|
+
let converted: any;
|
|
107
|
+
switch (option) {
|
|
108
|
+
case "shell":
|
|
109
|
+
converted = snippet.convert("shell", "curl");
|
|
110
|
+
break;
|
|
111
|
+
case "js":
|
|
112
|
+
converted = snippet.convert("javascript", "fetch");
|
|
113
|
+
break;
|
|
114
|
+
case "python":
|
|
115
|
+
converted = snippet.convert("python", "requests");
|
|
116
|
+
break;
|
|
117
|
+
case "java":
|
|
118
|
+
converted = snippet.convert("java", "okhttp");
|
|
119
|
+
break;
|
|
120
|
+
case "go":
|
|
121
|
+
converted = snippet.convert("go", "native");
|
|
122
|
+
break;
|
|
123
|
+
case "csharp":
|
|
124
|
+
converted = snippet.convert("csharp", "httpclient");
|
|
125
|
+
break;
|
|
126
|
+
case "kotlin":
|
|
127
|
+
converted = snippet.convert("kotlin", "okhttp");
|
|
128
|
+
break;
|
|
129
|
+
case "objc":
|
|
130
|
+
converted = snippet.convert("objc", "nsurlsession");
|
|
131
|
+
break;
|
|
132
|
+
case "php":
|
|
133
|
+
converted = snippet.convert("php", "http2");
|
|
134
|
+
break;
|
|
135
|
+
case "ruby":
|
|
136
|
+
converted = snippet.convert("ruby");
|
|
137
|
+
break;
|
|
138
|
+
case "swift":
|
|
139
|
+
converted = snippet.convert("swift");
|
|
140
|
+
break;
|
|
141
|
+
default:
|
|
142
|
+
converted = snippet.convert("shell");
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return converted ? converted[0] : "";
|
|
147
|
+
};
|