@lukoweb/apitogo 0.2.14 → 0.2.16
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.
|
@@ -31,4 +31,5 @@ export type MDXImport = {
|
|
|
31
31
|
__filepath: string;
|
|
32
32
|
default: (props: MDXProps) => JSX.Element;
|
|
33
33
|
};
|
|
34
|
+
export declare const withIndexAliases: <T>(fileImports: Record<string, T>) => [string, T][];
|
|
34
35
|
export declare const markdownPlugin: (options: MarkdownPluginOptions) => ApitogoPlugin;
|
package/package.json
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { glob } from "glob";
|
|
3
|
+
import globParent from "glob-parent";
|
|
4
|
+
|
|
5
|
+
export type HomeRedirect = { from: string; to: string };
|
|
6
|
+
|
|
7
|
+
const IGNORED = ["**/node_modules/**", "**/dist/**", "**/.git/**"];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The routes the docs plugin gives the pages these globs match: the path inside the glob's folder, without the
|
|
11
|
+
* extension. Missing folders and negated globs are fine, they just match nothing.
|
|
12
|
+
*/
|
|
13
|
+
export async function docRoutes(patterns: string[]): Promise<string[]> {
|
|
14
|
+
const excluded = patterns
|
|
15
|
+
.filter((pattern) => pattern.startsWith("!"))
|
|
16
|
+
.map((pattern) => pattern.slice(1));
|
|
17
|
+
const included = patterns.filter((pattern) => !pattern.startsWith("!"));
|
|
18
|
+
|
|
19
|
+
const routes = await Promise.all(
|
|
20
|
+
included.map(async (pattern) => {
|
|
21
|
+
const files = await glob(pattern, {
|
|
22
|
+
absolute: true,
|
|
23
|
+
posix: true,
|
|
24
|
+
ignore: [...IGNORED, ...excluded],
|
|
25
|
+
});
|
|
26
|
+
const parent = globParent(pattern);
|
|
27
|
+
return files.map(
|
|
28
|
+
(file) =>
|
|
29
|
+
`/${path.posix.relative(parent, file).replace(/\.mdx?$/, "")}`,
|
|
30
|
+
);
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
return [...new Set(routes.flat())].sort();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The page a visitor lands on when the site has no home page: the top-most page, then the first by name. */
|
|
37
|
+
export function pickLandingRoute(routes: string[]): string | undefined {
|
|
38
|
+
const depth = (route: string) => route.split("/").length;
|
|
39
|
+
return [...routes].sort(
|
|
40
|
+
(a, b) => depth(a) - depth(b) || a.localeCompare(b),
|
|
41
|
+
)[0];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type HomeRedirectInput = {
|
|
45
|
+
/** APItoGo builds the docs pages. */
|
|
46
|
+
hostsDocs: boolean;
|
|
47
|
+
/** Routes of those pages (see `docRoutes`). */
|
|
48
|
+
docRoutes: string[];
|
|
49
|
+
/** Where the docs live when someone else hosts them. */
|
|
50
|
+
externalDocsUrl?: string;
|
|
51
|
+
/** The portal has an API reference page, at this path. */
|
|
52
|
+
apiReferencePath?: string;
|
|
53
|
+
/** The portal has console pages (sign-in, keys, plans). */
|
|
54
|
+
userPanel: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Where `/` should go when the portal has no page of its own there. Without this a visitor of the home page sees
|
|
59
|
+
* "Page not found".
|
|
60
|
+
*
|
|
61
|
+
* - Hosted docs without an index page: the first page, so the team's own docs never need an extra file.
|
|
62
|
+
* - Hosted docs without any page: the API reference, when there is one.
|
|
63
|
+
* - External docs and no console: the external docs site, as the portal has nothing else to show.
|
|
64
|
+
*/
|
|
65
|
+
export function homeRedirect(
|
|
66
|
+
input: HomeRedirectInput,
|
|
67
|
+
): HomeRedirect | undefined {
|
|
68
|
+
if (input.hostsDocs) {
|
|
69
|
+
if (input.docRoutes.some((route) => route === "/" || route === "/index"))
|
|
70
|
+
return undefined;
|
|
71
|
+
const landing = pickLandingRoute(input.docRoutes);
|
|
72
|
+
if (landing) return { from: "/", to: landing };
|
|
73
|
+
return input.apiReferencePath
|
|
74
|
+
? { from: "/", to: input.apiReferencePath }
|
|
75
|
+
: undefined;
|
|
76
|
+
}
|
|
77
|
+
if (input.externalDocsUrl && !input.userPanel) {
|
|
78
|
+
return { from: "/", to: input.externalDocsUrl };
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
package/src/config/v2/wrapper.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ReferenceSpecError,
|
|
6
6
|
renderReferenceSpec,
|
|
7
7
|
} from "./gateway-reference.js";
|
|
8
|
+
import { docRoutes, type HomeRedirect, homeRedirect } from "./home-redirect.js";
|
|
8
9
|
import {
|
|
9
10
|
hasCapability,
|
|
10
11
|
missingManifestServices,
|
|
@@ -243,6 +244,7 @@ export function buildSiteConfig(
|
|
|
243
244
|
resolved: ResolvedConfigV2,
|
|
244
245
|
plan: PortalPlan,
|
|
245
246
|
theme?: unknown,
|
|
247
|
+
redirects: HomeRedirect[] = [],
|
|
246
248
|
): Record<string, unknown> {
|
|
247
249
|
const portal = resolved.portal;
|
|
248
250
|
return {
|
|
@@ -283,6 +285,7 @@ export function buildSiteConfig(
|
|
|
283
285
|
],
|
|
284
286
|
}
|
|
285
287
|
: {}),
|
|
288
|
+
...(redirects.length > 0 ? { redirects } : {}),
|
|
286
289
|
...(theme !== undefined ? { theme } : {}),
|
|
287
290
|
...(plan.auth ? { authentication: { type: "openid", ...plan.auth } } : {}),
|
|
288
291
|
...(plan.preview ? { preview: true } : {}),
|
|
@@ -430,7 +433,19 @@ export async function preparePortal(
|
|
|
430
433
|
});
|
|
431
434
|
}
|
|
432
435
|
}
|
|
433
|
-
const
|
|
436
|
+
const home = homeRedirect({
|
|
437
|
+
hostsDocs: plan.docs,
|
|
438
|
+
docRoutes: plan.docs ? await docRoutes(profile.docs.content) : [],
|
|
439
|
+
externalDocsUrl: profile.docs.url,
|
|
440
|
+
apiReferencePath: plan.apiReference ? `/${API_REFERENCE_PATH}` : undefined,
|
|
441
|
+
userPanel: plan.userPanel,
|
|
442
|
+
});
|
|
443
|
+
const siteConfig = buildSiteConfig(
|
|
444
|
+
resolved,
|
|
445
|
+
plan,
|
|
446
|
+
await readTheme(resolved),
|
|
447
|
+
home ? [home] : [],
|
|
448
|
+
);
|
|
434
449
|
const wrapper = renderWrapper(plan);
|
|
435
450
|
const wrapperPath = path.join(resolved.state.generatedDir, WRAPPER_FILE);
|
|
436
451
|
if (options.write !== false) {
|
|
@@ -35,11 +35,31 @@ export type MDXImport = {
|
|
|
35
35
|
default: (props: MDXProps) => JSX.Element;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
const INDEX_SUFFIX = "/index";
|
|
39
|
+
|
|
40
|
+
// `index.mdx` is the page of its folder: `/index` also answers at `/`, `/guides/index` at `/guides`.
|
|
41
|
+
// Without this the browser has no route for `/` (the pre-built index.html only hides that on first load).
|
|
42
|
+
export const withIndexAliases = <T,>(
|
|
43
|
+
fileImports: Record<string, T>,
|
|
44
|
+
): [string, T][] => {
|
|
45
|
+
const entries = Object.entries(fileImports);
|
|
46
|
+
return entries.flatMap(([routePath, value]): [string, T][] => {
|
|
47
|
+
if (!routePath.endsWith(INDEX_SUFFIX)) return [[routePath, value]];
|
|
48
|
+
const alias = routePath.slice(0, -INDEX_SUFFIX.length) || "/";
|
|
49
|
+
return alias in fileImports
|
|
50
|
+
? [[routePath, value]]
|
|
51
|
+
: [
|
|
52
|
+
[routePath, value],
|
|
53
|
+
[alias, value],
|
|
54
|
+
];
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
38
58
|
export const markdownPlugin = (
|
|
39
59
|
options: MarkdownPluginOptions,
|
|
40
60
|
): ApitogoPlugin => ({
|
|
41
61
|
getRoutes: () => {
|
|
42
|
-
return
|
|
62
|
+
return withIndexAliases(options.fileImports).map(
|
|
43
63
|
([routePath, importPromise]) => ({
|
|
44
64
|
path: routePath,
|
|
45
65
|
lazy: async () => {
|