@apimatic/cli 2.0.0-beta.4 → 2.0.0-beta.6
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/README.md +20 -64
- package/apimatic.schema.json +2 -2
- package/lib/actions/action-result.d.ts +4 -2
- package/lib/actions/action-result.js +8 -3
- package/lib/actions/action-result.js.map +1 -1
- package/lib/actions/api/transform.d.ts +7 -6
- package/lib/actions/api/transform.js +12 -13
- package/lib/actions/api/transform.js.map +1 -1
- package/lib/actions/api/validate.d.ts +4 -5
- package/lib/actions/api/validate.js +30 -32
- package/lib/actions/api/validate.js.map +1 -1
- package/lib/actions/portal/generate.js +3 -3
- package/lib/actions/portal/generate.js.map +1 -1
- package/lib/actions/portal/prepare-project.js +3 -0
- package/lib/actions/portal/prepare-project.js.map +1 -1
- package/lib/actions/quickstart.d.ts +1 -9
- package/lib/actions/quickstart.js +122 -102
- package/lib/actions/quickstart.js.map +1 -1
- package/lib/commands/api/transform.d.ts +1 -1
- package/lib/commands/api/transform.js +2 -2
- package/lib/commands/api/transform.js.map +1 -1
- package/lib/commands/api/validate.d.ts +3 -3
- package/lib/commands/api/validate.js +28 -17
- package/lib/commands/api/validate.js.map +1 -1
- package/lib/commands/plugin/generate.d.ts +1 -1
- package/lib/commands/portal/generate.d.ts +2 -2
- package/lib/commands/portal/generate.js +3 -1
- package/lib/commands/portal/generate.js.map +1 -1
- package/lib/commands/portal/serve.d.ts +2 -2
- package/lib/commands/portal/serve.js +1 -1
- package/lib/commands/sdk/generate.d.ts +1 -1
- package/lib/infrastructure/file-service.d.ts +5 -0
- package/lib/infrastructure/file-service.js +40 -9
- package/lib/infrastructure/file-service.js.map +1 -1
- package/lib/infrastructure/portal-dev-server-service.js +2 -1
- package/lib/infrastructure/portal-dev-server-service.js.map +1 -1
- package/lib/infrastructure/portal-project-service.d.ts +2 -0
- package/lib/infrastructure/portal-project-service.js +9 -4
- package/lib/infrastructure/portal-project-service.js.map +1 -1
- package/lib/infrastructure/services/file-download-service.d.ts +4 -4
- package/lib/infrastructure/services/file-download-service.js +14 -14
- package/lib/infrastructure/services/file-download-service.js.map +1 -1
- package/lib/prompts/api/transform.d.ts +0 -2
- package/lib/prompts/api/transform.js +0 -3
- package/lib/prompts/api/transform.js.map +1 -1
- package/lib/prompts/api/validate.d.ts +2 -2
- package/lib/prompts/api/validate.js +35 -3
- package/lib/prompts/api/validate.js.map +1 -1
- package/lib/prompts/format.d.ts +1 -1
- package/lib/prompts/format.js.map +1 -1
- package/lib/prompts/portal/generate.d.ts +2 -1
- package/lib/prompts/portal/generate.js +22 -6
- package/lib/prompts/portal/generate.js.map +1 -1
- package/lib/prompts/portal/prepare-project.d.ts +5 -1
- package/lib/prompts/portal/prepare-project.js +15 -1
- package/lib/prompts/portal/prepare-project.js.map +1 -1
- package/lib/prompts/portal/serve.js +6 -3
- package/lib/prompts/portal/serve.js.map +1 -1
- package/lib/prompts/portal/source.d.ts +6 -6
- package/lib/prompts/portal/source.js +97 -31
- package/lib/prompts/portal/source.js.map +1 -1
- package/lib/prompts/quickstart.d.ts +14 -13
- package/lib/prompts/quickstart.js +43 -27
- package/lib/prompts/quickstart.js.map +1 -1
- package/lib/types/apimatic-config/document.d.ts +2 -0
- package/lib/types/apimatic-config/document.js +4 -0
- package/lib/types/apimatic-config/document.js.map +1 -1
- package/lib/types/file/directory.js +2 -1
- package/lib/types/file/directory.js.map +1 -1
- package/lib/types/file/directoryPath.d.ts +3 -0
- package/lib/types/file/directoryPath.js +10 -1
- package/lib/types/file/directoryPath.js.map +1 -1
- package/lib/types/file/fileName.d.ts +5 -0
- package/lib/types/file/fileName.js +13 -3
- package/lib/types/file/fileName.js.map +1 -1
- package/lib/types/file/filePath.js +1 -1
- package/lib/types/file/filePath.js.map +1 -1
- package/lib/types/file/resource-input.d.ts +7 -5
- package/lib/types/file/resource-input.js +16 -15
- package/lib/types/file/resource-input.js.map +1 -1
- package/lib/types/flags-provider.d.ts +6 -1
- package/lib/types/flags-provider.js +22 -12
- package/lib/types/flags-provider.js.map +1 -1
- package/lib/types/plugin-config-context.d.ts +3 -0
- package/lib/types/plugin-config-context.js +33 -12
- package/lib/types/plugin-config-context.js.map +1 -1
- package/lib/types/portal/code-samples.d.ts +2 -0
- package/lib/types/portal/code-samples.js +7 -0
- package/lib/types/portal/code-samples.js.map +1 -1
- package/lib/types/portal/config/site-address.d.ts +16 -0
- package/lib/types/portal/config/site-address.js +67 -0
- package/lib/types/portal/config/site-address.js.map +1 -0
- package/lib/types/portal/config/site-config.d.ts +3 -5
- package/lib/types/portal/config/site-config.js +10 -33
- package/lib/types/portal/config/site-config.js.map +1 -1
- package/lib/types/portal/config/static-asset.d.ts +2 -2
- package/lib/types/portal/config/static-asset.js +2 -2
- package/lib/types/portal/config/static-asset.js.map +1 -1
- package/lib/types/portal/content-tree.d.ts +11 -4
- package/lib/types/portal/content-tree.js +31 -20
- package/lib/types/portal/content-tree.js.map +1 -1
- package/lib/types/portal/endpoint.d.ts +1 -0
- package/lib/types/portal/endpoint.js +3 -0
- package/lib/types/portal/endpoint.js.map +1 -1
- package/lib/types/portal/openapi-document.d.ts +4 -0
- package/lib/types/portal/openapi-document.js +12 -0
- package/lib/types/portal/openapi-document.js.map +1 -1
- package/lib/types/portal/page.js +16 -1
- package/lib/types/portal/page.js.map +1 -1
- package/lib/types/portal/portal-config.d.ts +6 -3
- package/lib/types/portal/portal-config.js +8 -4
- package/lib/types/portal/portal-config.js.map +1 -1
- package/lib/types/portal/portal-languages.d.ts +2 -0
- package/lib/types/portal/portal-languages.js +5 -1
- package/lib/types/portal/portal-languages.js.map +1 -1
- package/lib/types/portal/portal-source.d.ts +43 -11
- package/lib/types/portal/shared.d.ts +11 -0
- package/lib/types/portal/shared.js +10 -0
- package/lib/types/portal/shared.js.map +1 -0
- package/lib/types/portal/spec-metadata.d.ts +17 -0
- package/lib/types/portal/spec-metadata.js +40 -0
- package/lib/types/portal/spec-metadata.js.map +1 -0
- package/lib/types/portal-source-context.d.ts +21 -8
- package/lib/types/portal-source-context.js +208 -50
- package/lib/types/portal-source-context.js.map +1 -1
- package/lib/types/project-context.d.ts +18 -0
- package/lib/types/project-context.js +18 -1
- package/lib/types/project-context.js.map +1 -1
- package/lib/types/project-layout.d.ts +3 -0
- package/lib/types/project-layout.js +4 -0
- package/lib/types/project-layout.js.map +1 -1
- package/lib/types/resource-context.d.ts +25 -10
- package/lib/types/resource-context.js +40 -23
- package/lib/types/resource-context.js.map +1 -1
- package/oclif.manifest.json +88 -70
- package/package.json +1 -1
- package/portal-template/dependency-directories.ts +31 -40
- package/portal-template/downloads.ts +6 -3
- package/portal-template/portal-config.ts +4 -0
- package/portal-template/prerender-pages.ts +8 -2
- package/portal-template/spec-reload.ts +12 -9
- package/portal-template/src/components/api-markdown.tsx +32 -0
- package/portal-template/src/components/api-page.tsx +8 -1
- package/portal-template/src/components/authorization-failure.tsx +63 -0
- package/portal-template/src/components/markdown-copy-button.tsx +2 -2
- package/portal-template/src/components/plugin-install.tsx +6 -5
- package/portal-template/src/components/sdk-actions.tsx +3 -2
- package/portal-template/src/components/sdk-cards.tsx +2 -1
- package/portal-template/src/components/search.tsx +9 -2
- package/portal-template/src/lib/base-path.ts +14 -0
- package/portal-template/src/lib/code-titles.ts +8 -0
- package/portal-template/src/lib/copy-markdown.ts +4 -2
- package/portal-template/src/lib/install-command.ts +5 -5
- package/portal-template/src/lib/layout.shared.tsx +5 -3
- package/portal-template/src/lib/llms.server.ts +29 -5
- package/portal-template/src/lib/oauth-callback.ts +63 -0
- package/portal-template/src/lib/portal-types.ts +3 -3
- package/portal-template/src/lib/rehype-code-titles.ts +3 -5
- package/portal-template/src/lib/remark-image-references.ts +35 -0
- package/portal-template/src/lib/search-content.ts +28 -0
- package/portal-template/src/lib/search-options.ts +7 -0
- package/portal-template/src/lib/source.server.ts +2 -0
- package/portal-template/src/router.tsx +3 -1
- package/portal-template/src/routes/$.tsx +3 -2
- package/portal-template/src/routes/__root.tsx +8 -1
- package/portal-template/src/routes/api/search[.]json.ts +23 -2
- package/portal-template/src/routes/oauth.callback.tsx +54 -0
- package/portal-template/static-functions-base.ts +41 -0
- package/portal-template/vite.config.ts +15 -8
|
@@ -1,24 +1,27 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import type { Plugin } from 'vite';
|
|
3
2
|
|
|
3
|
+
/** The extensions of the files a specification is read from and refers to: the CLI's own list, held to it by a test. */
|
|
4
|
+
export const SPEC_FILE_EXTENSIONS = ['.json', '.yaml', '.yml'];
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
|
-
* Reloads the preview when a file
|
|
6
|
-
* at run time rather than imported, so Vite's
|
|
7
|
-
* reference pages are built once per
|
|
7
|
+
* Reloads the preview when a specification file in the specification directory changes, as a document may refer
|
|
8
|
+
* to parts kept in another folder there. The specifications are read at run time rather than imported, so Vite's
|
|
9
|
+
* module graph never learns of them, and the reference pages are built once per evaluation.
|
|
8
10
|
*/
|
|
9
|
-
export function specReload(
|
|
10
|
-
const
|
|
11
|
-
|
|
11
|
+
export function specReload(specDirectory: string): Plugin {
|
|
12
|
+
const isSpecFile = (file: string) =>
|
|
13
|
+
file.startsWith(`${specDirectory}/`) &&
|
|
14
|
+
SPEC_FILE_EXTENSIONS.some((extension) => file.toLowerCase().endsWith(extension));
|
|
12
15
|
|
|
13
16
|
return {
|
|
14
17
|
name: 'apimatic:spec-reload',
|
|
15
18
|
apply: 'serve',
|
|
16
19
|
configureServer(server) {
|
|
17
|
-
server.watcher.add(
|
|
20
|
+
server.watcher.add(specDirectory);
|
|
18
21
|
},
|
|
19
22
|
// Sent to each environment: the server runner re-evaluates every module, the browser reloads.
|
|
20
23
|
hotUpdate({ file }) {
|
|
21
|
-
if (!
|
|
24
|
+
if (!isSpecFile(file)) return;
|
|
22
25
|
this.environment.hot.send({ type: 'full-reload' });
|
|
23
26
|
return [];
|
|
24
27
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createMarkdownRenderer } from 'fumadocs-core/content/md';
|
|
2
|
+
import { remarkGfm } from 'fumadocs-core/mdx-plugins/remark-gfm';
|
|
3
|
+
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock';
|
|
4
|
+
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
|
5
|
+
import { Children, type ComponentProps, type ReactElement } from 'react';
|
|
6
|
+
import { withBasePath } from '@/lib/base-path';
|
|
7
|
+
import { languageOf } from '@/lib/code-titles';
|
|
8
|
+
|
|
9
|
+
const { Markdown } = createMarkdownRenderer({ remarkPlugins: [remarkGfm] });
|
|
10
|
+
|
|
11
|
+
const components = {
|
|
12
|
+
...defaultMdxComponents,
|
|
13
|
+
// A root-relative address names a file in the static directory, which the host serves under the base.
|
|
14
|
+
img: ({ src, ...props }: ComponentProps<'img'>) => (
|
|
15
|
+
<img {...props} src={typeof src === 'string' && /^\/(?!\/)/.test(src) ? withBasePath(src) : src} />
|
|
16
|
+
),
|
|
17
|
+
pre: CodeBlock
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** The specification's descriptions as Fumadocs renders them, but for its `img: undefined`, which fails the page. */
|
|
21
|
+
export function ApiMarkdown({ md }: { md: string }) {
|
|
22
|
+
return <Markdown components={components}>{md}</Markdown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A fenced block, highlighted as Fumadocs highlights one in a description. */
|
|
26
|
+
function CodeBlock({ children }: ComponentProps<'pre'>) {
|
|
27
|
+
const code = Children.only(children) as ReactElement<{ className?: string; children?: unknown }>;
|
|
28
|
+
const text = code.props.children;
|
|
29
|
+
if (typeof text !== 'string') return null;
|
|
30
|
+
const language = languageOf(code.props.className?.split(' ') ?? []) ?? 'text';
|
|
31
|
+
return <DynamicCodeBlock lang={language} code={text.trimEnd()} />;
|
|
32
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { createOpenAPIPage } from 'fumadocs-openapi/ui';
|
|
2
|
+
import { withBasePath } from '@/lib/base-path';
|
|
2
3
|
import { mediaAdapters } from '@/lib/media-adapters';
|
|
4
|
+
import { oauthCallbackPath } from '@/lib/oauth-callback';
|
|
5
|
+
import { ApiMarkdown } from './api-markdown';
|
|
3
6
|
import { renderExampleLayout } from './example-layout';
|
|
4
7
|
import { codeUsages, renderUsageTabs } from './usage-tabs';
|
|
5
8
|
|
|
@@ -7,5 +10,9 @@ export const OpenAPIPage = createOpenAPIPage({
|
|
|
7
10
|
codeUsages,
|
|
8
11
|
generateTypeScriptDefinitions: false,
|
|
9
12
|
content: { renderAPIExampleLayout: renderExampleLayout, renderAPIExampleUsageTabs: renderUsageTabs },
|
|
10
|
-
|
|
13
|
+
components: { Markdown: ApiMarkdown },
|
|
14
|
+
mediaAdapters,
|
|
15
|
+
oauthRedirectUrl: withBasePath(oauthCallbackPath),
|
|
16
|
+
// Portals under paths of one host keep their playground's saved values apart; at the root the keys stay Fumadocs' own.
|
|
17
|
+
storageKeyPrefix: `fumadocs-openapi-${withBasePath('/').slice(1)}`
|
|
11
18
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { withBasePath } from '@/lib/base-path';
|
|
4
|
+
import type { OAuthCallbackFailure } from '@/lib/oauth-callback';
|
|
5
|
+
|
|
6
|
+
/** What the OAuth callback says when it stays, instead of sending the browser back to the endpoint. */
|
|
7
|
+
export function AuthorizationFailure({ failure }: { failure: OAuthCallbackFailure }) {
|
|
8
|
+
switch (failure.kind) {
|
|
9
|
+
case 'providerError':
|
|
10
|
+
// Ended, not refused: `server_error` and `temporarily_unavailable` arrive here too.
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<Explanation>
|
|
14
|
+
The provider ended the authorization with <code>{failure.error}</code>
|
|
15
|
+
{failure.description === null ? '.' : `: ${failure.description}`}
|
|
16
|
+
</Explanation>
|
|
17
|
+
<Action href={failure.page}>Back to the endpoint</Action>
|
|
18
|
+
</>
|
|
19
|
+
);
|
|
20
|
+
case 'notStarted':
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<Explanation>
|
|
24
|
+
This page completes an authorization started from an endpoint's playground, and none was started in this
|
|
25
|
+
browser. Open the endpoint's page and authorize from there.
|
|
26
|
+
</Explanation>
|
|
27
|
+
<HomeAction />
|
|
28
|
+
</>
|
|
29
|
+
);
|
|
30
|
+
case 'unknownPage':
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<Explanation>
|
|
34
|
+
The authorization was not started from a page of this site, so it cannot be completed here. Open the
|
|
35
|
+
endpoint's page and authorize from there.
|
|
36
|
+
</Explanation>
|
|
37
|
+
<HomeAction />
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function Explanation({ children }: { children: ReactNode }) {
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<h1 className="text-2xl font-semibold">Authorization could not be completed</h1>
|
|
47
|
+
<p className="text-fd-muted-foreground max-w-md">{children}</p>
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The page that started the authorization is unknown here, so the home page is the one way back.
|
|
53
|
+
function HomeAction() {
|
|
54
|
+
return <Action href={withBasePath('/')}>Go to the home page</Action>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function Action({ href, children }: { href: string; children: ReactNode }) {
|
|
58
|
+
return (
|
|
59
|
+
<a href={href} className={buttonVariants({ color: 'primary', className: 'mt-4' })}>
|
|
60
|
+
{children}
|
|
61
|
+
</a>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -12,7 +12,7 @@ const LABELS: Record<CopyState, string> = {
|
|
|
12
12
|
failed: 'Copy failed'
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
export function MarkdownCopyButton({ markdownUrl
|
|
15
|
+
export function MarkdownCopyButton({ markdownUrl }: Readonly<{ markdownUrl: string }>) {
|
|
16
16
|
const [state, setState] = useState<CopyState>('idle');
|
|
17
17
|
|
|
18
18
|
useEffect(() => {
|
|
@@ -23,7 +23,7 @@ export function MarkdownCopyButton({ markdownUrl, base }: Readonly<{ markdownUrl
|
|
|
23
23
|
|
|
24
24
|
const copy = () => {
|
|
25
25
|
setState('copying');
|
|
26
|
-
copyMarkdown(markdownUrl
|
|
26
|
+
copyMarkdown(markdownUrl).then(
|
|
27
27
|
() => setState('copied'),
|
|
28
28
|
(error: unknown) => {
|
|
29
29
|
console.error(error);
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { useSyncExternalStore } from 'react';
|
|
2
2
|
import { asMarkdown } from 'fumadocs-core/server';
|
|
3
3
|
import { CommandBlock } from './command-block';
|
|
4
|
+
import { fullAddress } from '@/lib/base-path';
|
|
4
5
|
import { installCommand } from '@/lib/install-command';
|
|
5
6
|
import { portal } from '@/lib/portal';
|
|
6
7
|
|
|
7
|
-
// The
|
|
8
|
+
// The address never changes while the page is open, so there is nothing to subscribe to.
|
|
8
9
|
const noSubscribe = () => () => {};
|
|
9
10
|
|
|
10
|
-
/** The prerender cannot know where the portal is hosted, so the browser swaps in its own
|
|
11
|
+
/** The prerender cannot know where the portal is hosted, so the browser swaps in its own address. */
|
|
11
12
|
export function PluginInstall({ path }: Readonly<{ path: string }>) {
|
|
12
13
|
// Ahead of the hook, which only React may call; either branch is taken in every render of one environment.
|
|
13
14
|
if (asMarkdown()) {
|
|
14
15
|
return <CommandBlock command={installCommand(path, portal.siteUrl)} />;
|
|
15
16
|
}
|
|
16
|
-
const
|
|
17
|
+
const site = useSyncExternalStore(
|
|
17
18
|
noSubscribe,
|
|
18
|
-
() =>
|
|
19
|
+
() => fullAddress('/'),
|
|
19
20
|
() => portal.siteUrl
|
|
20
21
|
);
|
|
21
|
-
return <CommandBlock command={installCommand(path,
|
|
22
|
+
return <CommandBlock command={installCommand(path, site)} />;
|
|
22
23
|
}
|
|
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
import { asMarkdown } from 'fumadocs-core/server';
|
|
3
3
|
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
|
4
4
|
import { Code, Download, Package } from 'lucide-react';
|
|
5
|
+
import { withBasePath } from '@/lib/base-path';
|
|
5
6
|
|
|
6
7
|
export interface SdkActionsProps {
|
|
7
8
|
download: string;
|
|
@@ -22,7 +23,7 @@ export function SdkActions({ download, source, packageUrl, registry }: Readonly<
|
|
|
22
23
|
// The page's Markdown twin, which the page actions and llms-full.txt hand to an AI assistant.
|
|
23
24
|
if (asMarkdown()) {
|
|
24
25
|
const links = [
|
|
25
|
-
`[Download SDK](${download})`,
|
|
26
|
+
`[Download SDK](${withBasePath(download)})`,
|
|
26
27
|
source ? `[View source](${source})` : null,
|
|
27
28
|
packageUrl ? `[View on ${registry}](${packageUrl})` : null
|
|
28
29
|
];
|
|
@@ -64,7 +65,7 @@ interface LinkProps {
|
|
|
64
65
|
/** A plain link, not a route: the zip is a file the site serves beside its pages. */
|
|
65
66
|
export function DownloadLink({ href, className, children }: Readonly<LinkProps>) {
|
|
66
67
|
return (
|
|
67
|
-
<a href={href} download className={className}>
|
|
68
|
+
<a href={withBasePath(href)} download className={className}>
|
|
68
69
|
{children}
|
|
69
70
|
</a>
|
|
70
71
|
);
|
|
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
import Link from 'fumadocs-core/link';
|
|
3
3
|
import { asMarkdown } from 'fumadocs-core/server';
|
|
4
4
|
import { ArrowDownToLine, ArrowRight, ExternalLink as ExternalLinkIcon } from 'lucide-react';
|
|
5
|
+
import { withBasePath } from '@/lib/base-path';
|
|
5
6
|
import { CommandBlock } from './command-block';
|
|
6
7
|
import { LanguageLogo, LogoTile } from './logos';
|
|
7
8
|
import { DownloadLink, ExternalLink, LINK, QUIET_LINK, SdkActions, type SdkActionsProps } from './sdk-actions';
|
|
@@ -40,7 +41,7 @@ function LinkContext({ name }: Readonly<{ name: string }>) {
|
|
|
40
41
|
export function SdkCard({ language, name, page, install, ...actions }: Readonly<SdkCardProps>) {
|
|
41
42
|
if (asMarkdown()) {
|
|
42
43
|
const command = install ? `\`${install}\` · ` : '';
|
|
43
|
-
return [`- [${name}](${page}): ${command}`, <SdkActions key="actions" {...actions} />];
|
|
44
|
+
return [`- [${name}](${withBasePath(page)}): ${command}`, <SdkActions key="actions" {...actions} />];
|
|
44
45
|
}
|
|
45
46
|
const { download, source, packageUrl, registry } = actions;
|
|
46
47
|
return (
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
import { useDocsSearch } from 'fumadocs-core/search/client';
|
|
14
14
|
import { staticClient } from 'fumadocs-core/search/client/orama-static';
|
|
15
15
|
import { useI18n } from 'fumadocs-ui/contexts/i18n';
|
|
16
|
+
import { useMemo } from 'react';
|
|
17
|
+
import { withBasePath } from '@/lib/base-path';
|
|
16
18
|
|
|
17
19
|
export default function DefaultSearchDialog(props: SharedProps) {
|
|
18
20
|
const { locale } = useI18n();
|
|
@@ -20,10 +22,15 @@ export default function DefaultSearchDialog(props: SharedProps) {
|
|
|
20
22
|
// Served with an extension so hosts and CDNs give it a JSON content type; without one
|
|
21
23
|
// it went out as application/octet-stream, which some of them attach or refuse.
|
|
22
24
|
client: staticClient({
|
|
23
|
-
from: '/api/search.json',
|
|
25
|
+
from: withBasePath('/api/search.json'),
|
|
24
26
|
locale
|
|
25
27
|
})
|
|
26
28
|
});
|
|
29
|
+
// Served URLs, since the dialog navigates by href (search-navigation.test.ts), memoised so its highlight stays put.
|
|
30
|
+
const results = useMemo(
|
|
31
|
+
() => (query.data === 'empty' ? null : query.data?.map((item) => ({ ...item, url: withBasePath(item.url) }))),
|
|
32
|
+
[query.data]
|
|
33
|
+
);
|
|
27
34
|
|
|
28
35
|
return (
|
|
29
36
|
<SearchDialog search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>
|
|
@@ -34,7 +41,7 @@ export default function DefaultSearchDialog(props: SharedProps) {
|
|
|
34
41
|
<SearchDialogInput />
|
|
35
42
|
<SearchDialogClose />
|
|
36
43
|
</SearchDialogHeader>
|
|
37
|
-
<SearchDialogList items={
|
|
44
|
+
<SearchDialogList items={results} />
|
|
38
45
|
</SearchDialogContent>
|
|
39
46
|
</SearchDialog>
|
|
40
47
|
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
// Undefined outside Vite, as under the template's unit tests; Fumadocs guards it the same way.
|
|
4
|
+
const BASE = typeof import.meta.env === 'undefined' ? '/' : import.meta.env.BASE_URL;
|
|
5
|
+
|
|
6
|
+
/** A portal-relative path as the host serves it, under `base`: Vite's own unless one is given. */
|
|
7
|
+
export function withBasePath(path: string, base: string = BASE): string {
|
|
8
|
+
return `${base.replace(/\/$/, '')}${path}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A portal-relative path's full address as the browser sees it; the path itself where there is no window. */
|
|
12
|
+
export function fullAddress(path: string): string {
|
|
13
|
+
return typeof window === 'undefined' ? path : new URL(withBasePath(path), window.location.origin).toString();
|
|
14
|
+
}
|
|
@@ -27,3 +27,11 @@ export const CODE_BLOCK_TITLES: Readonly<Record<BundledLanguage, string>> = {
|
|
|
27
27
|
sql: 'SQL',
|
|
28
28
|
tsx: 'TSX'
|
|
29
29
|
};
|
|
30
|
+
|
|
31
|
+
const LANGUAGE_CLASS = 'language-';
|
|
32
|
+
|
|
33
|
+
/** The language a fence names, from the class `remark-rehype` gives its `code`. */
|
|
34
|
+
export function languageOf(classes: unknown[]): string | undefined {
|
|
35
|
+
const languageClass = classes.find((name) => typeof name === 'string' && name.startsWith(LANGUAGE_CLASS));
|
|
36
|
+
return typeof languageClass === 'string' ? languageClass.slice(LANGUAGE_CLASS.length) : undefined;
|
|
37
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { withBasePath } from './base-path';
|
|
2
|
+
|
|
3
|
+
export async function copyMarkdown(rootRelativeUrl: string, base?: string): Promise<void> {
|
|
4
|
+
const url = withBasePath(rootRelativeUrl, base);
|
|
3
5
|
// Handed over unresolved, so Safari still counts the write as part of the click.
|
|
4
6
|
const markdown = fetchMarkdown(url);
|
|
5
7
|
await Promise.all([navigator.clipboard.write([new ClipboardItem({ 'text/plain': markdown })]), markdown]);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
const ABSOLUTE = /^https?:\/\//i;
|
|
2
2
|
|
|
3
3
|
// Quoted, so a `&` or a space in the address stays one argument in any shell a reader pastes it into.
|
|
4
|
-
export function installCommand(path: string,
|
|
5
|
-
return `npx context-plugins install "${installAddress(path,
|
|
4
|
+
export function installCommand(path: string, site: string | null): string {
|
|
5
|
+
return `npx context-plugins install "${installAddress(path, site)}"`;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
function installAddress(path: string,
|
|
9
|
-
if (ABSOLUTE.test(path) ||
|
|
8
|
+
function installAddress(path: string, site: string | null): string {
|
|
9
|
+
if (ABSOLUTE.test(path) || site === null) {
|
|
10
10
|
return path;
|
|
11
11
|
}
|
|
12
|
-
return `${
|
|
12
|
+
return `${site.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
|
|
13
13
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
|
2
|
+
import { withBasePath } from './base-path';
|
|
2
3
|
import { portal } from './portal';
|
|
3
4
|
|
|
4
5
|
export function baseOptions(): BaseLayoutProps {
|
|
@@ -30,12 +31,13 @@ const LOGO_SIZE = 'h-6 w-auto max-w-none';
|
|
|
30
31
|
function Logo() {
|
|
31
32
|
const { logo } = portal;
|
|
32
33
|
if (!logo) return null;
|
|
33
|
-
|
|
34
|
+
const [light, dark] = [withBasePath(logo.light), withBasePath(logo.dark)];
|
|
35
|
+
if (light === dark) return <img src={light} alt="" className={LOGO_SIZE} />;
|
|
34
36
|
// Both are in the page and CSS shows one, so the right one is there before any script runs.
|
|
35
37
|
return (
|
|
36
38
|
<>
|
|
37
|
-
<img src={
|
|
38
|
-
<img src={
|
|
39
|
+
<img src={light} alt="" className={`${LOGO_SIZE} dark:hidden`} />
|
|
40
|
+
<img src={dark} alt="" className={`hidden ${LOGO_SIZE} dark:block`} />
|
|
39
41
|
</>
|
|
40
42
|
);
|
|
41
43
|
}
|
|
@@ -1,15 +1,35 @@
|
|
|
1
|
+
import type * as PageTree from 'fumadocs-core/page-tree';
|
|
1
2
|
import { llms } from 'fumadocs-core/source';
|
|
2
3
|
import { stringify } from 'yaml';
|
|
3
4
|
import { getMDXComponents } from '@/components/mdx';
|
|
5
|
+
import { withBasePath } from './base-path';
|
|
4
6
|
import { operationLabels } from './openapi-labels';
|
|
5
7
|
import { slimOpenAPIPageProps } from './openapi-slim';
|
|
6
|
-
import { source } from './source.server';
|
|
8
|
+
import { type PortalPage, source } from './source.server';
|
|
7
9
|
import { portal } from './portal';
|
|
8
10
|
|
|
9
|
-
type PortalPage = ReturnType<typeof source.getPages>[number];
|
|
10
11
|
type ReferencePage = Extract<PortalPage, { type: 'openapi' }>;
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
// `llms()` links each page by its tree node's URL and has no base of its own; titles are found by `$ref`.
|
|
14
|
+
const index = llms({
|
|
15
|
+
...source,
|
|
16
|
+
getPageTree: (locale?: string) => {
|
|
17
|
+
const root = source.getPageTree(locale);
|
|
18
|
+
return { ...root, children: root.children.map(servedNode) };
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function servedNode(node: PageTree.Node): PageTree.Node {
|
|
23
|
+
if (node.type === 'page') return { ...node, url: withBasePath(node.url) };
|
|
24
|
+
if (node.type === 'folder') {
|
|
25
|
+
return {
|
|
26
|
+
...node,
|
|
27
|
+
index: node.index && { ...node.index, url: withBasePath(node.index.url) },
|
|
28
|
+
children: node.children.map(servedNode)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return node;
|
|
32
|
+
}
|
|
13
33
|
|
|
14
34
|
export async function renderPage(page: PortalPage): Promise<string> {
|
|
15
35
|
return await render(page, true);
|
|
@@ -34,14 +54,18 @@ export function renderHome(): string {
|
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
async function renderContent(page: Exclude<PortalPage, ReferencePage>): Promise<string> {
|
|
37
|
-
return `# ${page.data.title} (${page.url})\n\n${await page.data.getText('processed', { components: getMDXComponents() })}`;
|
|
57
|
+
return `# ${page.data.title} (${withBasePath(page.url)})\n\n${await page.data.getText('processed', { components: getMDXComponents() })}`;
|
|
38
58
|
}
|
|
39
59
|
|
|
40
60
|
/** An operation's page, with the part of its specification that page renders: what an assistant reads best. */
|
|
41
61
|
function renderReference(page: ReferencePage, withSpecification: boolean): string {
|
|
42
62
|
const props = slimOpenAPIPageProps(page.data.getOpenAPIPageProps());
|
|
43
63
|
const endpoints = operationLabels(props, (signature) => `\`${signature}\``);
|
|
44
|
-
const sections = [
|
|
64
|
+
const sections = [
|
|
65
|
+
`# ${page.data.title} (${withBasePath(page.url)})`,
|
|
66
|
+
endpoints.join('\n'),
|
|
67
|
+
page.data.description ?? ''
|
|
68
|
+
];
|
|
45
69
|
if (withSpecification) {
|
|
46
70
|
// Its tags and the document's description are the portal's to show, and would come with every operation.
|
|
47
71
|
const { info, tags: _tags, ...document } = props.payload.bundled as Record<string, unknown> & {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one redirect URI a portal owner registers with their OAuth provider, instead of the
|
|
3
|
+
* address of every endpoint page that can start an authorization. The CLI keeps the user's
|
|
4
|
+
* pages off it, in `src/types/portal/content-tree.ts`.
|
|
5
|
+
*/
|
|
6
|
+
export const oauthCallbackPath = '/oauth/callback';
|
|
7
|
+
|
|
8
|
+
/** The cookie the playground writes before it leaves, naming the page that started the flow. */
|
|
9
|
+
const PAGE_COOKIE_NAME = 'fumadocs-openapi-oauth';
|
|
10
|
+
const PAGE_COOKIE = new RegExp(`(?:^|;\\s*)${PAGE_COOKIE_NAME}=([^;]+)`);
|
|
11
|
+
|
|
12
|
+
/** What the callback does with the browser that lands on it. */
|
|
13
|
+
export type OAuthCallback =
|
|
14
|
+
/** Sends it back to the page that started the flow, with the provider's answer still attached. */
|
|
15
|
+
| { kind: 'return'; url: string }
|
|
16
|
+
/** Stays, to show the error the provider ended the authorization with, and a link back to that page. */
|
|
17
|
+
| { kind: 'providerError'; error: string; description: string | null; page: string }
|
|
18
|
+
/** Stays, since the cookie names no page: no authorization was started in this browser. */
|
|
19
|
+
| { kind: 'notStarted' }
|
|
20
|
+
/** Stays, since the cookie names a page on another site, or one that cannot be read. */
|
|
21
|
+
| { kind: 'unknownPage' };
|
|
22
|
+
|
|
23
|
+
/** Every outcome but sending the browser on, which leaves the callback before it renders again. */
|
|
24
|
+
export type OAuthCallbackFailure = Exclude<OAuthCallback, { kind: 'return' }>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The provider answers in the query for the authorization code flow and in the fragment for the
|
|
28
|
+
* implicit one, an error included. Only an authorization this browser started is answered with
|
|
29
|
+
* the provider's error: anyone can link here with an `error_description` of their own.
|
|
30
|
+
*/
|
|
31
|
+
export function oauthCallback(cookie: string, location: Pick<Location, 'origin' | 'search' | 'hash'>): OAuthCallback {
|
|
32
|
+
const value = PAGE_COOKIE.exec(cookie)?.[1];
|
|
33
|
+
if (value === undefined) return { kind: 'notStarted' };
|
|
34
|
+
|
|
35
|
+
let page: URL;
|
|
36
|
+
try {
|
|
37
|
+
page = new URL(decodeURIComponent(value), location.origin);
|
|
38
|
+
} catch {
|
|
39
|
+
return { kind: 'unknownPage' };
|
|
40
|
+
}
|
|
41
|
+
// Anything else is an open redirect, carrying the authorization code to another site.
|
|
42
|
+
if (page.origin !== location.origin) return { kind: 'unknownPage' };
|
|
43
|
+
|
|
44
|
+
const query = new URLSearchParams(location.search);
|
|
45
|
+
const answer = query.has('error') ? query : new URLSearchParams(location.hash.slice(1));
|
|
46
|
+
const error = answer.get('error');
|
|
47
|
+
if (error !== null) {
|
|
48
|
+
return { kind: 'providerError', error, description: answer.get('error_description'), page: page.href };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Whole, not as a path: a path of `//evil.test/x` would be read as another site's address.
|
|
52
|
+
page.search = location.search;
|
|
53
|
+
page.hash = location.hash;
|
|
54
|
+
return { kind: 'return', url: page.href };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The playground leaves the cookie for the session, so a later visit would be sent back to the
|
|
59
|
+
* last page that started an authorization. Forgotten once read, it names only the flow just started.
|
|
60
|
+
*/
|
|
61
|
+
export function forgetStartingPage(): void {
|
|
62
|
+
document.cookie = `${PAGE_COOKIE_NAME}=; path=/; max-age=0`;
|
|
63
|
+
}
|
|
@@ -10,14 +10,14 @@ export interface PortalLink {
|
|
|
10
10
|
external: boolean;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
/**
|
|
13
|
+
/** Portal-relative, one per colour mode; the same URL twice when one image serves both. */
|
|
14
14
|
export interface PortalLogo {
|
|
15
15
|
light: string;
|
|
16
16
|
dark: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface PortalFavicon {
|
|
20
|
-
/**
|
|
20
|
+
/** Portal-relative, and inside the static directory. */
|
|
21
21
|
url: string;
|
|
22
22
|
type: string | null;
|
|
23
23
|
}
|
|
@@ -25,7 +25,7 @@ export interface PortalFavicon {
|
|
|
25
25
|
export interface Portal {
|
|
26
26
|
name: string;
|
|
27
27
|
description: string | null;
|
|
28
|
-
/**
|
|
28
|
+
/** The site address, with no trailing slash. */
|
|
29
29
|
siteUrl: string | null;
|
|
30
30
|
logo: PortalLogo | null;
|
|
31
31
|
favicon: PortalFavicon | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseCodeBlockAttributes } from 'fumadocs-core/mdx-plugins/codeblock-utils';
|
|
2
|
-
import { CODE_BLOCK_TITLES } from './code-titles';
|
|
2
|
+
import { CODE_BLOCK_TITLES, languageOf } from './code-titles';
|
|
3
3
|
import { bundledLanguage } from './shiki-bundle';
|
|
4
4
|
|
|
5
5
|
interface HastNode {
|
|
@@ -11,8 +11,6 @@ interface HastNode {
|
|
|
11
11
|
children?: HastNode[];
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const LANGUAGE_CLASS = 'language-';
|
|
15
|
-
|
|
16
14
|
/** Where `remarkCodeTab` puts a tabbed block, by its own tabs or an MDX `<Tabs>`: the tab already names it. */
|
|
17
15
|
const CODE_TABS = new Set(['CodeBlockTab', 'Tab', 'TabsContent']);
|
|
18
16
|
|
|
@@ -40,8 +38,8 @@ export function rehypeCodeTitles() {
|
|
|
40
38
|
return;
|
|
41
39
|
}
|
|
42
40
|
const classes = Array.isArray(code.properties?.className) ? code.properties.className : [];
|
|
43
|
-
const
|
|
44
|
-
const language =
|
|
41
|
+
const name = languageOf(classes);
|
|
42
|
+
const language = name === undefined ? null : bundledLanguage(name);
|
|
45
43
|
const meta = typeof code.properties?.metastring === 'string' ? code.properties.metastring : '';
|
|
46
44
|
// Read as `rehypeCode` reads it, which shows no title for a bare or unquoted one.
|
|
47
45
|
const { title } = parseCodeBlockAttributes(meta, ['title']).attributes;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** An mdast node, as far as images and the definitions they refer to go. */
|
|
2
|
+
interface MarkdownNode {
|
|
3
|
+
type: string;
|
|
4
|
+
identifier?: string;
|
|
5
|
+
url?: string;
|
|
6
|
+
title?: string | null;
|
|
7
|
+
alt?: string | null;
|
|
8
|
+
position?: unknown;
|
|
9
|
+
children?: MarkdownNode[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Writes `![alt][ref]` as ``, the only form Fumadocs' `remarkImage` imports. page.ts checks the same. */
|
|
13
|
+
export function remarkImageReferences() {
|
|
14
|
+
return (tree: MarkdownNode) => {
|
|
15
|
+
const definitions = new Map<string, MarkdownNode>();
|
|
16
|
+
const define = (node: MarkdownNode) => {
|
|
17
|
+
// The first definition of a label wins, as CommonMark reads them.
|
|
18
|
+
if (node.type === 'definition' && node.identifier !== undefined && !definitions.has(node.identifier)) {
|
|
19
|
+
definitions.set(node.identifier, node);
|
|
20
|
+
}
|
|
21
|
+
node.children?.forEach(define);
|
|
22
|
+
};
|
|
23
|
+
define(tree);
|
|
24
|
+
|
|
25
|
+
const inline = (node: MarkdownNode): MarkdownNode => {
|
|
26
|
+
const definition = node.type === 'imageReference' ? definitions.get(node.identifier ?? '') : undefined;
|
|
27
|
+
if (definition !== undefined) {
|
|
28
|
+
return { type: 'image', url: definition.url, title: definition.title, alt: node.alt, position: node.position };
|
|
29
|
+
}
|
|
30
|
+
if (node.children !== undefined) node.children = node.children.map(inline);
|
|
31
|
+
return node;
|
|
32
|
+
};
|
|
33
|
+
inline(tree);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { remark } from 'remark';
|
|
2
|
+
|
|
3
|
+
/** An mdast node, as far as finding images goes. */
|
|
4
|
+
interface MarkdownNode {
|
|
5
|
+
type: string;
|
|
6
|
+
position?: { start: { offset?: number }; end: { offset?: number } };
|
|
7
|
+
children?: MarkdownNode[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const IMAGES = new Set(['image', 'imageReference']);
|
|
11
|
+
|
|
12
|
+
// Without GFM, as the search dialog renders a result.
|
|
13
|
+
const markdown = remark();
|
|
14
|
+
|
|
15
|
+
/** Markdown without its images, as Fumadocs indexes a page of its own, and otherwise as written. */
|
|
16
|
+
export function withoutImages(text: string): string {
|
|
17
|
+
let kept = '';
|
|
18
|
+
let from = 0;
|
|
19
|
+
for (const { position } of imagesIn(markdown.parse(text))) {
|
|
20
|
+
kept += text.slice(from, position?.start.offset ?? from);
|
|
21
|
+
from = position?.end.offset ?? from;
|
|
22
|
+
}
|
|
23
|
+
return kept + text.slice(from);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function imagesIn(node: MarkdownNode): MarkdownNode[] {
|
|
27
|
+
return IMAGES.has(node.type) ? [node] : (node.children ?? []).flatMap(imagesIn);
|
|
28
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { parseSearchWith, stringifySearchWith } from '@tanstack/react-router';
|
|
2
|
+
|
|
3
|
+
/** Query values as text: under a base path the router rewrites the address through these, and JSON loses some. */
|
|
4
|
+
export const searchOptions = {
|
|
5
|
+
parseSearch: parseSearchWith((value) => value),
|
|
6
|
+
stringifySearch: stringifySearchWith(JSON.stringify)
|
|
7
|
+
};
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { createRouter as createTanStackRouter } from '@tanstack/react-router';
|
|
2
2
|
import { routeTree } from './routeTree.gen';
|
|
3
3
|
import { NotFound } from '@/components/not-found';
|
|
4
|
+
import { searchOptions } from '@/lib/search-options';
|
|
4
5
|
|
|
5
6
|
export function getRouter() {
|
|
6
7
|
return createTanStackRouter({
|
|
7
8
|
routeTree,
|
|
8
9
|
defaultPreload: 'intent',
|
|
9
10
|
scrollRestoration: true,
|
|
10
|
-
defaultNotFoundComponent: NotFound
|
|
11
|
+
defaultNotFoundComponent: NotFound,
|
|
12
|
+
...searchOptions
|
|
11
13
|
});
|
|
12
14
|
}
|