Skip to content

Commit d6660e3

Browse files
jagarnicanebrius
andauthored
feat: add tryitnow for usage snippets through code samples API (#20)
* add basic piping for try it now * add npmPackageName as a string * remove type from global * clean up types, use separate state for usageSnippets * add optimization regarding chunks * move snippet logic to top-level function * set default theme to dark * formatting changes * move logic for CodeSnippets closer to rendering of data * remove flag for npm package * Fleshed out settings so that try-it-now is optional * Reverted SnippetAI formatting changes --------- Co-authored-by: nebrius <[email protected]>
1 parent 2db634d commit d6660e3

File tree

12 files changed

+148
-10
lines changed

12 files changed

+148
-10
lines changed

packages/docs-md/assets/TryItNow/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export const TryItNow = ({
9292
entry: "index.tsx",
9393
...sandpackSetupOptions,
9494
}}
95-
theme="auto"
95+
theme="dark"
9696
>
9797
<SandpackLayout>
9898
<SandpackPreview style={styles.preview} />

packages/docs-md/src/cli/cli.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const CONFIG_FILE_NAMES = [
2626
];
2727

2828
const args = arg({
29+
"--npm-package-name": String,
2930
"--help": Boolean,
3031
"--config": String,
3132
"--spec": String,
@@ -37,6 +38,7 @@ const args = arg({
3738
"-p": "--page-out-dir",
3839
"-o": "--component-out-dir",
3940
"-f": "--framework",
41+
"-n": "--npm-package-name",
4042
});
4143

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

5457
if (args["--help"]) {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { basename } from "node:path";
2+
3+
import type { Chunk, OperationChunk } from "../types/chunk.ts";
4+
import type {
5+
CodeSamplesResponse,
6+
CodeSnippet,
7+
ErrorResponse,
8+
} from "../types/codeSnippet.ts";
9+
import { getSettings } from "./settings.ts";
10+
11+
const CODE_SNIPPETS_API_URL = "http://localhost:35290";
12+
13+
export type DocsCodeSnippets = Record<OperationChunk["id"], CodeSnippet>;
14+
15+
export const generateDocsCodeSnippets = async (
16+
docsData: Map<string, Chunk>,
17+
specContents: string
18+
): Promise<DocsCodeSnippets> => {
19+
const { spec, tryItNow } = getSettings();
20+
if (!tryItNow) {
21+
return {};
22+
}
23+
24+
const docsCodeSnippets: DocsCodeSnippets = {};
25+
26+
const specFilename = basename(spec);
27+
// create a by operationId map of the operation chunks
28+
const operationChunksByOperationId = new Map<string, OperationChunk>();
29+
for (const chunk of docsData.values()) {
30+
if (chunk.chunkType === "operation") {
31+
operationChunksByOperationId.set(chunk.chunkData.operationId, chunk);
32+
}
33+
}
34+
try {
35+
const formData = new FormData();
36+
37+
const blob = new Blob([specContents]);
38+
formData.append("language", "typescript");
39+
formData.append("schema_file", blob, specFilename);
40+
formData.append("package_name", tryItNow.npmPackageName);
41+
formData.append("sdk_class_name", tryItNow.sdkClassName);
42+
43+
const res = await fetch(`${CODE_SNIPPETS_API_URL}/v1/code_sample/preview`, {
44+
method: "POST",
45+
body: formData,
46+
});
47+
48+
const json = (await res.json()) as unknown;
49+
50+
if (!res.ok) {
51+
const error = json as ErrorResponse;
52+
throw new Error(`Failed to generate code sample: ${error.message}`);
53+
}
54+
const codeSnippets = (json as CodeSamplesResponse).snippets;
55+
56+
for (const snippet of codeSnippets) {
57+
const chunk = operationChunksByOperationId.get(snippet.operationId);
58+
// only set the usage snippet if the operation id exists in the spec
59+
if (chunk) {
60+
docsCodeSnippets[chunk.id] = snippet;
61+
}
62+
}
63+
} catch (error) {
64+
console.error("There was an error generating code snippets", error);
65+
return {};
66+
}
67+
return docsCodeSnippets;
68+
};

packages/docs-md/src/generator/docsData/getDocsData.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { fileURLToPath } from "node:url";
77
import { unzipSync } from "node:zlib";
88

99
import type { Chunk } from "../../types/chunk.ts";
10-
1110
declare class Go {
1211
argv: string[];
1312
env: { [envKey: string]: string };
@@ -32,8 +31,10 @@ export async function getDocsData(
3231
const result = await WebAssembly.instantiate(wasmBuffer, go.importObject);
3332
void go.run(result.instance);
3433
const serializedDocsData = await SerializeDocsData(specContents);
34+
3535
const docsData = (JSON.parse(serializedDocsData) as string[]).map(
3636
(chunk) => JSON.parse(chunk) as Chunk
3737
);
38+
3839
return new Map(docsData.map((chunk) => [chunk.id, chunk]));
3940
}

packages/docs-md/src/generator/generatePages.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Settings } from "../types/settings.ts";
2+
import { generateDocsCodeSnippets } from "./codeSnippets.ts";
23
import { getDocsData } from "./docsData/getDocsData.ts";
34
import { generateContent } from "./mdx/generateContent.ts";
45
import { setSettings } from "./settings.ts";
@@ -18,9 +19,14 @@ export async function generatePages({
1819
setSettings(settings);
1920

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

25+
// Get code snippets
26+
console.log("Generating code snippets");
27+
const docsCodeSnippets = await generateDocsCodeSnippets(data, specContents);
28+
2329
// Generate the content
2430
console.log("Generating Markdown pages");
25-
return generateContent(data);
31+
return generateContent(data, docsCodeSnippets);
2632
}

packages/docs-md/src/generator/mdx/chunks/operation.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { Chunk, OperationChunk } from "../../../types/chunk.ts";
2+
import type { DocsCodeSnippets } from "../../codeSnippets.ts";
3+
import { getSettings } from "../../settings.ts";
24
import type { Renderer, Site } from "../renderer.ts";
35
import { getSchemaFromId } from "../util.ts";
46
import { renderSchema } from "./schema.ts";
@@ -9,6 +11,7 @@ type RenderOperationOptions = {
911
chunk: OperationChunk;
1012
docsData: Map<string, Chunk>;
1113
baseHeadingLevel: number;
14+
docsCodeSnippets: DocsCodeSnippets;
1215
};
1316

1417
export function renderOperation({
@@ -17,6 +20,7 @@ export function renderOperation({
1720
chunk,
1821
docsData,
1922
baseHeadingLevel,
23+
docsCodeSnippets,
2024
}: RenderOperationOptions) {
2125
renderer.appendHeading(
2226
baseHeadingLevel,
@@ -90,6 +94,22 @@ export function renderOperation({
9094
}
9195
}
9296

97+
const { tryItNow } = getSettings();
98+
99+
const usageSnippet = docsCodeSnippets[chunk.id];
100+
if (usageSnippet && tryItNow) {
101+
renderer.appendHeading(baseHeadingLevel + 1, "Try it Now");
102+
// TODO: Zod is actually hard coded for now since its always a dependency
103+
// in our SDKs. Ideally this will come from the SDK package.
104+
renderer.appendTryItNow({
105+
externalDependencies: {
106+
zod: "^3.25.64",
107+
[tryItNow.npmPackageName]: "latest",
108+
},
109+
defaultValue: usageSnippet.code,
110+
});
111+
}
112+
93113
if (chunk.chunkData.requestBody) {
94114
renderer.appendHeading(
95115
baseHeadingLevel + 1,

packages/docs-md/src/generator/mdx/generateContent.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { join, resolve } from "node:path";
22

33
import type { Chunk, SchemaChunk, TagChunk } from "../../types/chunk.ts";
44
import { assertNever } from "../../util/assertNever.ts";
5+
import type { DocsCodeSnippets } from "../codeSnippets.ts";
56
import { getSettings } from "../settings.ts";
67
import { renderAbout } from "./chunks/about.ts";
78
import { renderOperation } from "./chunks/operation.ts";
@@ -144,7 +145,12 @@ function getPageMap(data: Data) {
144145
return pageMap;
145146
}
146147

147-
function renderPages(site: Site, pageMap: PageMap, data: Map<string, Chunk>) {
148+
function renderPages(
149+
site: Site,
150+
pageMap: PageMap,
151+
data: Map<string, Chunk>,
152+
docsCodeSnippets: DocsCodeSnippets
153+
) {
148154
for (const [currentPagePath, pageMapEntry] of pageMap) {
149155
if (pageMapEntry.type === "renderer") {
150156
const renderer = site.createPage(currentPagePath);
@@ -195,6 +201,7 @@ function renderPages(site: Site, pageMap: PageMap, data: Map<string, Chunk>) {
195201
chunk,
196202
docsData: data,
197203
baseHeadingLevel: 2,
204+
docsCodeSnippets,
198205
});
199206
break;
200207
}
@@ -280,13 +287,15 @@ function renderScaffoldSupport(site: Site) {
280287
}
281288
}
282289

283-
export function generateContent(data: Data): Record<string, string> {
290+
export function generateContent(
291+
data: Data,
292+
docsCodeSnippets: DocsCodeSnippets
293+
): Record<string, string> {
284294
// First, get a mapping of pages to chunks
285295
const pageMap = getPageMap(data);
286-
287296
// Then, render each page
288297
const site = new Site();
289-
renderPages(site, pageMap, data);
298+
renderPages(site, pageMap, data, docsCodeSnippets);
290299

291300
// Now do any post-processing needed by the scaffold
292301
renderScaffoldSupport(site);

packages/docs-md/src/generator/mdx/renderer.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,12 @@ sidebarTitle: ${this.escapeText(sidebarLabel)}
231231

232232
// TODO: need to type this properly, but we can't import types from assets
233233
// since they can't be built as part of this TS project
234-
public appendTryItNow(props: Record<string, unknown> = {}) {
234+
public appendTryItNow(
235+
props: {
236+
externalDependencies?: Record<string, string>;
237+
defaultValue?: string;
238+
} & Record<string, unknown>
239+
) {
235240
this.insertComponentImport("TryItNow", "TryItNow/index.tsx");
236241
const escapedProps = Object.fromEntries(
237242
Object.entries(props).map(([key, value]) => [
@@ -241,7 +246,9 @@ sidebarTitle: ${this.escapeText(sidebarLabel)}
241246
: JSON.stringify(value),
242247
])
243248
);
244-
this.#lines.push(`<TryItNow {...${JSON.stringify(escapedProps)}} />`);
249+
this.#lines.push(
250+
`<TryItNow {...${JSON.stringify(escapedProps)}} externalDependencies={${JSON.stringify(props.externalDependencies)}} defaultValue={\`${props.defaultValue}\`} />`
251+
);
245252
}
246253

247254
public finalize() {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export type CodeSnippet = {
2+
operationId: string;
3+
language: string;
4+
code: string;
5+
};
6+
7+
export type CodeSamplesResponse = {
8+
snippets: CodeSnippet[];
9+
};
10+
11+
export type ErrorResponse = {
12+
message: string;
13+
statusCode: number;
14+
};

packages/docs-md/src/types/settings.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ export const settingsSchema = z.strictObject({
2626
suggestions: z.array(z.string()).optional(),
2727
})
2828
.optional(),
29+
tryItNow: z
30+
.strictObject({
31+
npmPackageName: z.string(),
32+
sdkClassName: z.string(),
33+
})
34+
.optional(),
2935
});
3036

3137
export type Settings = z.infer<typeof settingsSchema>;

0 commit comments

Comments
 (0)