Skip to content

Commit

Permalink
feat: add an interactive playground to the documentation site
Browse files Browse the repository at this point in the history
  • Loading branch information
mnahkies committed Dec 27, 2024
1 parent d982546 commit 513b30c
Show file tree
Hide file tree
Showing 18 changed files with 1,528 additions and 29 deletions.
15 changes: 15 additions & 0 deletions packages/documentation/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import nextra from "nextra"
import NodePolyfillPlugin from "node-polyfill-webpack-plugin"

/** @type {import('next').NextConfig} */
const nextConfig = {
Expand All @@ -8,6 +9,20 @@ const nextConfig = {
images: {
unoptimized: true,
},
webpack: (config, {buildId, dev, isServer, defaultLoaders, webpack}) => {
config.experiments = {...config.experiments, syncWebAssembly: true}
config.plugins.push(new NodePolyfillPlugin())
config.module.rules.push({
test: /node:.+/i,
use: "null-loader",
})
config.module.rules.push({
test: /@biomejs\/wasm-nodejs/i,
use: "null-loader",
})

return config
},
async redirects() {
return [{source: "/", destination: "/overview/about", permanent: false}]
},
Expand Down
6 changes: 6 additions & 0 deletions packages/documentation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@
"publish:gh-pages": "yarn gh-pages -d ./dist -b gh-pages --cname openapi-code-generator.nahkies.co.nz --nojekyll"
},
"dependencies": {
"@monaco-editor/react": "^4.6.0",
"@nahkies/openapi-code-generator": "*",
"monaco-editor": "^0.52.2",
"monaco-editor-auto-typings": "^0.4.6",
"next": "15.1.2",
"nextra": "^3.3.0",
"nextra-theme-docs": "^3.3.0",
"node-polyfill-webpack-plugin": "^4.1.0",
"react": "19.0.0",
"react-dom": "19.0.0"
},
Expand All @@ -35,6 +40,7 @@
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"gh-pages": "^6.2.0",
"null-loader": "^4.0.1",
"typescript": "^5.7.2"
}
}
215 changes: 215 additions & 0 deletions packages/documentation/src/lib/playground.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import {loadRuntimeTypes} from "@/lib/playground/load-runtime-types"
import {useHost} from "@/lib/playground/use-host"
import {useIsDirty} from "@/lib/playground/use-is-dirty"
import {Editor, type Monaco} from "@monaco-editor/react"
import type {Config} from "@nahkies/openapi-code-generator"
import type {editor} from "monaco-editor"
import {
AutoTypings,
LocalStorageCache,
} from "monaco-editor-auto-typings/custom-editor"
import {useCallback, useEffect, useState} from "react"

type IStandaloneCodeEditor = editor.IStandaloneCodeEditor

const ConfigForm: React.FC<{
config: Config
setConfig: (config: Config) => void
setIsDirty: (value: boolean) => void
}> = ({config, setConfig, setIsDirty}) => {
return (
<>
<select
value={config.template}
onChange={(e) => {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
setConfig({...config, template: e.target.value as any})
setIsDirty(true)
}}
>
{[
"typescript-koa",
"typescript-fetch",
"typescript-axios",
"typescript-angular",
].map((it) => (
<option key={it} value={it}>
{it}
</option>
))}
</select>
</>
)
}

const Playground: React.FC<{
specifications: {filename: string; content: string}[]
}> = ({specifications}) => {
const fileRootPath = "file:///"
const [config, setConfig] = useState<Config>({
input: "/example.yaml",
output: "/generated",
template: "typescript-fetch",
inputType: "openapi3",
schemaBuilder: "zod",
enableRuntimeResponseValidation: true,
extractInlineSchemas: true,
allowUnusedImports: false,
groupingStrategy: "none",
tsAllowAny: false,
tsCompilerOptions: {exactOptionalPropertyTypes: false},
enableTypedBasePaths: true,
filenameConvention: "kebab-case",
tsServerImplementationMethod: "type",
} satisfies Config)

const host = useHost({specifications, config})
const [isDirty, setIsDirty] = useIsDirty(100)
const [monaco, setMonaco] = useState<Monaco>()
const [inputEditor, setInputEditor] = useState<IStandaloneCodeEditor>()
const [outputEditor, setOutputEditor] = useState<IStandaloneCodeEditor>()
const inputModel = monaco?.editor.getModel(monaco.Uri.file("/example.yaml"))
const outputModel =
host.selectedModel &&
monaco?.editor.getModel(monaco.Uri.file(host.selectedModel))

//region mount input/output editors
const onMountInput = useCallback((editor: editor.IStandaloneCodeEditor) => {
setInputEditor(editor)
}, [])

const onMountOutput = useCallback(
(editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
...monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
moduleResolution:
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
target: monaco.languages.typescript.ScriptTarget.ES2020,
allowSyntheticDefaultImports: true,
esModuleInterop: true,
rootDir: fileRootPath,
})

setOutputEditor(editor)
setMonaco(monaco)
},
[],
)
//endregion
//region load typescript definitions
useEffect(() => {
if (!monaco || !outputEditor) {
return
}

loadRuntimeTypes(monaco, config.template).then(() => {
return AutoTypings.create(outputEditor, {
monaco,
fileRootPath,
sourceCache: new LocalStorageCache(),
onUpdate: (update) => console.log("progress", update),
onError: (error) => console.error(error),
dontAdaptEditorOptions: true,
preloadPackages: true,
shareCache: true,
versions: {
zod: "3.24.1",
},
})
})
}, [config.template, monaco, outputEditor])
//endregion
//region subscribe to input changes
useEffect(() => {
if (!inputModel || !inputEditor) {
return
}

if (inputEditor.getModel() !== inputModel) {
console.info("setting input model")
inputEditor.setModel(inputModel)
}

const listener = inputModel.onDidChangeContent((e) => {
console.info("input changed")

const value = inputModel?.getValue()

if (host.onFileChanged && value) {
host.onFileChanged({
filename: "/example.yaml",
value,
})
setIsDirty(true)
}
})

return () => {
listener.dispose()
}
}, [inputEditor, inputModel, host, setIsDirty])
//endregion
//region set output model
useEffect(() => {
if (!outputEditor || !outputModel) {
return
}

if (outputEditor.getModel() !== outputModel) {
outputEditor.setModel(outputModel)
}
}, [outputEditor, outputModel])
//endregion
//region run generation
useEffect(() => {
if (host.isLoading || !isDirty) {
console.info("skipping generation", {isDirty, host})
return
}

console.info("run generation")
setIsDirty(false)
// TODO: communicate to user
host.generate().catch((err) => {
console.error("code generation failed", err)
})
}, [host, isDirty, setIsDirty])
//endregion

if (host.isLoading) {
// TODO: loading spinner
return null
}

return (
<>
<h2>Input</h2>
<ConfigForm
config={config}
setConfig={setConfig}
setIsDirty={setIsDirty}
/>
<Editor height="30vh" onMount={onMountInput} />

<h2>Output</h2>
<select
value={host.selectedModel}
onChange={(e) => host.setSelectedModel(e.target.value)}
>
{host.availableModels.map((it) => (
<option key={it} value={it}>
{it}
</option>
))}
</select>

<Editor
height="30vh"
defaultLanguage="typescript"
onMount={onMountOutput}
/>
</>
)
}

export default Playground
86 changes: 86 additions & 0 deletions packages/documentation/src/lib/playground/load-runtime-types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type {Monaco} from "@monaco-editor/react"

/**
* Hack: the monaco-editor-auto-typings/custom-editor package doesn't handle the "exports" field in package.json
* files correctly, and annoyingly even if modified to handle this, the typescript language service doesn't seem
* to like it. Let's manually fetch these and map to locations the service will understand.
* @param monaco
* @param template
*/
export const loadRuntimeTypes = async (
monaco: Monaco,
template: "typescript-fetch" | "typescript-axios" | "typescript-koa",
) => {
const files = {
"typescript-angular": [],
"typescript-fetch": [
{
uri: "https://unpkg.com/@nahkies/typescript-fetch-runtime@latest/package.json",
path: "/node_modules/@nahkies/typescript-fetch-runtime/package.json",
},
{
uri: "https://unpkg.com/@nahkies/typescript-fetch-runtime@latest/dist/main.d.ts",
path: "/node_modules/@nahkies/typescript-fetch-runtime/main.d.ts",
},
{
uri: "https://unpkg.com/@nahkies/typescript-fetch-runtime@latest/dist/zod.d.ts",
path: "/node_modules/@nahkies/typescript-fetch-runtime/zod.d.ts",
},
{
uri: "https://unpkg.com/@nahkies/typescript-fetch-runtime@latest/dist/joi.d.ts",
path: "/node_modules/@nahkies/typescript-fetch-runtime/joi.d.ts",
},
{
uri: "https://unpkg.com/@nahkies/typescript-fetch-runtime@latest/dist/common.d.ts",
path: "/node_modules/@nahkies/typescript-fetch-runtime/common.d.ts",
},
{
uri: "https://unpkg.com/@nahkies/typescript-fetch-runtime@latest/dist/types.d.ts",
path: "/node_modules/@nahkies/typescript-fetch-runtime/types.d.ts",
},
],
"typescript-axios": [
{
uri: "https://unpkg.com/@nahkies/typescript-axios-runtime@latest/package.json",
path: "/node_modules/@nahkies/typescript-axios-runtime/package.json",
},
{
uri: "https://unpkg.com/@nahkies/typescript-axios-runtime@latest/dist/main.d.ts",
path: "/node_modules/@nahkies/typescript-axios-runtime/main.d.ts",
},
],
"typescript-koa": [
{
uri: "https://unpkg.com/@nahkies/typescript-koa-runtime@latest/package.json",
path: "/node_modules/@nahkies/typescript-koa-runtime/package.json",
},
{
uri: "https://unpkg.com/@nahkies/typescript-koa-runtime@latest/dist/server.d.ts",
path: "/node_modules/@nahkies/typescript-koa-runtime/server.d.ts",
},
{
uri: "https://unpkg.com/@nahkies/typescript-koa-runtime@latest/dist/errors.d.ts",
path: "/node_modules/@nahkies/typescript-koa-runtime/errors.d.ts",
},
{
uri: "https://unpkg.com/@nahkies/typescript-koa-runtime@latest/dist/zod.d.ts",
path: "/node_modules/@nahkies/typescript-koa-runtime/zod.d.ts",
},
{
uri: "https://unpkg.com/@nahkies/typescript-koa-runtime@latest/dist/joi.d.ts",
path: "/node_modules/@nahkies/typescript-koa-runtime/joi.d.ts",
},
],
}

for (const file of files[template]) {
const uri = monaco.Uri.file(file.path)
if (!monaco.editor.getModel(uri)) {
// TODO: error handling
const source = await (await fetch(file.uri)).text()
console.info(`createModel for ${uri.path}`)
monaco.editor.createModel(source, "typescript", uri)
// monaco.languages.typescript.typescriptDefaults.addExtraLib(await source.text(), file.path)
}
}
}
Loading

0 comments on commit 513b30c

Please sign in to comment.