Skip to content
Merged
2 changes: 1 addition & 1 deletion packages/docs-md/assets/TryItNow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const TryItNow = ({
entry: "index.tsx",
...sandpackSetupOptions,
}}
theme="auto"
theme="dark"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting the default to dark for now due to an issue with the text color not changing when going from "light" mode to "dark" mode.

>
<SandpackLayout>
<SandpackPreview style={styles.preview} />
Expand Down
5 changes: 4 additions & 1 deletion packages/docs-md/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const CONFIG_FILE_NAMES = [
];

const args = arg({
"--npm-package-name": String,
"--help": Boolean,
"--config": String,
"--spec": String,
Expand All @@ -37,6 +38,7 @@ const args = arg({
"-p": "--page-out-dir",
"-o": "--component-out-dir",
"-f": "--framework",
"-n": "--npm-package-name",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sets the name of the npm package downloaded later in TryItNow

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"-n": "--npm-package-name",

Let's leave this flag out. Only about half of the settings are exposed by the CLI anyways, and I've been thinking about removing all but -c and -s anyways.

});

function printHelp() {
Expand All @@ -48,7 +50,8 @@ Options:
--spec, -s Path to OpenAPI spec
--page-out-dir, -p Output directory for page contents
--component-out-dir, -o Output directory for component contents
--framework, -f Framework to use (docusaurus, nextra)`);
--framework, -f Framework to use (docusaurus, nextra)
--npm-package-name, -n npm package to use for the SDK code snippets`);
}

if (args["--help"]) {
Expand Down
68 changes: 68 additions & 0 deletions packages/docs-md/src/generator/codeSnippets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { basename } from "node:path";

import type { Chunk, OperationChunk } from "../types/chunk.ts";
import type {
CodeSamplesResponse,
CodeSnippet,
ErrorResponse,
} from "../types/codeSnippet.ts";
import { getSettings } from "./settings.ts";

const CODE_SNIPPETS_API_URL = "http://localhost:35290";

export type DocsCodeSnippets = Record<OperationChunk["id"], CodeSnippet>;

export const generateDocsCodeSnippets = async (
docsData: Map<string, Chunk>,
specContents: string
): Promise<DocsCodeSnippets> => {
const { spec, tryItNow } = getSettings();
if (!tryItNow) {
return {};
}

const docsCodeSnippets: DocsCodeSnippets = {};

const specFilename = basename(spec);
// create a by operationId map of the operation chunks
const operationChunksByOperationId = new Map<string, OperationChunk>();
for (const chunk of docsData.values()) {
if (chunk.chunkType === "operation") {
operationChunksByOperationId.set(chunk.chunkData.operationId, chunk);
}
}
try {
const formData = new FormData();

const blob = new Blob([specContents]);
formData.append("language", "typescript");
formData.append("schema_file", blob, specFilename);
formData.append("package_name", tryItNow.npmPackageName);
formData.append("sdk_class_name", tryItNow.sdkClassName);

const res = await fetch(`${CODE_SNIPPETS_API_URL}/v1/code_sample/preview`, {
method: "POST",
body: formData,
});

const json = (await res.json()) as unknown;

if (!res.ok) {
const error = json as ErrorResponse;
throw new Error(`Failed to generate code sample: ${error.message}`);
}
const codeSnippets = (json as CodeSamplesResponse).snippets;

for (const snippet of codeSnippets) {
const chunk = operationChunksByOperationId.get(snippet.operationId);
// only set the usage snippet if the operation id exists in the spec
if (chunk) {
docsCodeSnippets[chunk.id] = snippet;
}
}
} catch (error) {
console.error("There was an error generating code snippets", error);
return {};
}
return docsCodeSnippets;
};
3 changes: 2 additions & 1 deletion packages/docs-md/src/generator/docsData/getDocsData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { fileURLToPath } from "node:url";
import { unzipSync } from "node:zlib";

import type { Chunk } from "../../types/chunk.ts";

declare class Go {
argv: string[];
env: { [envKey: string]: string };
Expand All @@ -32,8 +31,10 @@ export async function getDocsData(
const result = await WebAssembly.instantiate(wasmBuffer, go.importObject);
void go.run(result.instance);
const serializedDocsData = await SerializeDocsData(specContents);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh actually hang on, I just noticed we're now calling SerializeDocsData twice in this function. We shouldn't do that either, since again this function is expensive.

Instead, let's create a new top-level group of operations at src/generator/codeSnippets, and move all logic in there. Then, we call the top-level function (which takes in docs data) and returns code snippets, that we add between the getDocsData and generateContent functions in packages/docs-md/src/generator/generatePages.ts.


const docsData = (JSON.parse(serializedDocsData) as string[]).map(
(chunk) => JSON.parse(chunk) as Chunk
);

return new Map(docsData.map((chunk) => [chunk.id, chunk]));
}
8 changes: 7 additions & 1 deletion packages/docs-md/src/generator/generatePages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Settings } from "../types/settings.ts";
import { generateDocsCodeSnippets } from "./codeSnippets.ts";
import { getDocsData } from "./docsData/getDocsData.ts";
import { generateContent } from "./mdx/generateContent.ts";
import { setSettings } from "./settings.ts";
Expand All @@ -18,9 +19,14 @@ export async function generatePages({
setSettings(settings);

// Get the docs data from the spec
console.log("Getting docs data");
const data = await getDocsData(specContents);

// Get code snippets
console.log("Generating code snippets");
const docsCodeSnippets = await generateDocsCodeSnippets(data, specContents);

// Generate the content
console.log("Generating Markdown pages");
return generateContent(data);
return generateContent(data, docsCodeSnippets);
}
20 changes: 20 additions & 0 deletions packages/docs-md/src/generator/mdx/chunks/operation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { Chunk, OperationChunk } from "../../../types/chunk.ts";
import type { DocsCodeSnippets } from "../../codeSnippets.ts";
import { getSettings } from "../../settings.ts";
import type { Renderer, Site } from "../renderer.ts";
import { getSchemaFromId } from "../util.ts";
import { renderSchema } from "./schema.ts";
Expand All @@ -9,6 +11,7 @@ type RenderOperationOptions = {
chunk: OperationChunk;
docsData: Map<string, Chunk>;
baseHeadingLevel: number;
docsCodeSnippets: DocsCodeSnippets;
};

export function renderOperation({
Expand All @@ -17,6 +20,7 @@ export function renderOperation({
chunk,
docsData,
baseHeadingLevel,
docsCodeSnippets,
}: RenderOperationOptions) {
renderer.appendHeading(
baseHeadingLevel,
Expand Down Expand Up @@ -90,6 +94,22 @@ export function renderOperation({
}
}

const { tryItNow } = getSettings();

const usageSnippet = docsCodeSnippets[chunk.id];
if (usageSnippet && tryItNow) {
renderer.appendHeading(baseHeadingLevel + 1, "Try it Now");
// TODO: Zod is actually hard coded for now since its always a dependency
// in our SDKs. Ideally this will come from the SDK package.
renderer.appendTryItNow({
externalDependencies: {
zod: "^3.25.64",
[tryItNow.npmPackageName]: "latest",
},
defaultValue: usageSnippet.code,
});
}

if (chunk.chunkData.requestBody) {
renderer.appendHeading(
baseHeadingLevel + 1,
Expand Down
17 changes: 13 additions & 4 deletions packages/docs-md/src/generator/mdx/generateContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { join, resolve } from "node:path";

import type { Chunk, SchemaChunk, TagChunk } from "../../types/chunk.ts";
import { assertNever } from "../../util/assertNever.ts";
import type { DocsCodeSnippets } from "../codeSnippets.ts";
import { getSettings } from "../settings.ts";
import { renderAbout } from "./chunks/about.ts";
import { renderOperation } from "./chunks/operation.ts";
Expand Down Expand Up @@ -144,7 +145,12 @@ function getPageMap(data: Data) {
return pageMap;
}

function renderPages(site: Site, pageMap: PageMap, data: Map<string, Chunk>) {
function renderPages(
site: Site,
pageMap: PageMap,
data: Map<string, Chunk>,
docsCodeSnippets: DocsCodeSnippets
) {
for (const [currentPagePath, pageMapEntry] of pageMap) {
if (pageMapEntry.type === "renderer") {
const renderer = site.createPage(currentPagePath);
Expand Down Expand Up @@ -195,6 +201,7 @@ function renderPages(site: Site, pageMap: PageMap, data: Map<string, Chunk>) {
chunk,
docsData: data,
baseHeadingLevel: 2,
docsCodeSnippets,
});
break;
}
Expand Down Expand Up @@ -280,13 +287,15 @@ function renderScaffoldSupport(site: Site) {
}
}

export function generateContent(data: Data): Record<string, string> {
export function generateContent(
data: Data,
docsCodeSnippets: DocsCodeSnippets
): Record<string, string> {
// First, get a mapping of pages to chunks
const pageMap = getPageMap(data);

// Then, render each page
const site = new Site();
renderPages(site, pageMap, data);
renderPages(site, pageMap, data, docsCodeSnippets);

// Now do any post-processing needed by the scaffold
renderScaffoldSupport(site);
Expand Down
11 changes: 9 additions & 2 deletions packages/docs-md/src/generator/mdx/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,12 @@ sidebarTitle: ${this.escapeText(sidebarLabel)}

// TODO: need to type this properly, but we can't import types from assets
// since they can't be built as part of this TS project
public appendTryItNow(props: Record<string, unknown> = {}) {
public appendTryItNow(
props: {
externalDependencies?: Record<string, string>;
defaultValue?: string;
} & Record<string, unknown>
) {
this.insertComponentImport("TryItNow", "TryItNow/index.tsx");
const escapedProps = Object.fromEntries(
Object.entries(props).map(([key, value]) => [
Expand All @@ -241,7 +246,9 @@ sidebarTitle: ${this.escapeText(sidebarLabel)}
: JSON.stringify(value),
])
);
this.#lines.push(`<TryItNow {...${JSON.stringify(escapedProps)}} />`);
this.#lines.push(
`<TryItNow {...${JSON.stringify(escapedProps)}} externalDependencies={${JSON.stringify(props.externalDependencies)}} defaultValue={\`${props.defaultValue}\`} />`
);
}

public finalize() {
Expand Down
14 changes: 14 additions & 0 deletions packages/docs-md/src/types/codeSnippet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type CodeSnippet = {
operationId: string;
language: string;
code: string;
};

export type CodeSamplesResponse = {
snippets: CodeSnippet[];
};

export type ErrorResponse = {
message: string;
statusCode: number;
};
6 changes: 6 additions & 0 deletions packages/docs-md/src/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export const settingsSchema = z.strictObject({
suggestions: z.array(z.string()).optional(),
})
.optional(),
tryItNow: z
.strictObject({
npmPackageName: z.string(),
sdkClassName: z.string(),
})
.optional(),
});

export type Settings = z.infer<typeof settingsSchema>;
2 changes: 2 additions & 0 deletions scaffold-templates/docusaurus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"typecheck": "tsc"
},
"dependencies": {
"@codesandbox/sandpack-client": "^2.19.8",
"@codesandbox/sandpack-react": "^2.20.0",
"@docusaurus/core": "3.7.0",
"@docusaurus/preset-classic": "3.7.0",
"@mdx-js/react": "^3.0.0",
Expand Down
2 changes: 2 additions & 0 deletions scaffold-templates/nextra/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"license": "ISC",
"description": "",
"dependencies": {
"@codesandbox/sandpack-client": "^2.19.8",
"@codesandbox/sandpack-react": "^2.20.0",
"@radix-ui/colors": "^3.0.0",
"@speakeasy-api/docs-md": "^0.1.0-beta3",
"jotai": "^2.12.5",
Expand Down