-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add an interactive playground to the documentation site
- Loading branch information
Showing
18 changed files
with
1,528 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
86
packages/documentation/src/lib/playground/load-runtime-types.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
Oops, something went wrong.