@stainless-api/playgrounds 0.0.1-beta.0

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +23 -0
  3. package/eslint.config.js +2 -0
  4. package/package.json +69 -0
  5. package/src/Logs.tsx +216 -0
  6. package/src/Panel.tsx +21 -0
  7. package/src/PlaygroundPanelWrapper.tsx +5 -0
  8. package/src/build-py-types.ts +152 -0
  9. package/src/build-ts-types.ts +70 -0
  10. package/src/build.ts +97 -0
  11. package/src/codemirror/comlink.ts +698 -0
  12. package/src/codemirror/curl/curlconverter.vendor.js +7959 -0
  13. package/src/codemirror/curl.ts +108 -0
  14. package/src/codemirror/deps.ts +12 -0
  15. package/src/codemirror/fix-lsp-markdown.ts +50 -0
  16. package/src/codemirror/lsp.ts +87 -0
  17. package/src/codemirror/python/anser.ts +398 -0
  18. package/src/codemirror/python/pyodide.ts +180 -0
  19. package/src/codemirror/python.ts +160 -0
  20. package/src/codemirror/react.tsx +615 -0
  21. package/src/codemirror/sanitize-html.ts +12 -0
  22. package/src/codemirror/shiki.ts +65 -0
  23. package/src/codemirror/typescript/cdn-typescript.d.ts +1 -0
  24. package/src/codemirror/typescript/cdn-typescript.js +1 -0
  25. package/src/codemirror/typescript/console.ts +590 -0
  26. package/src/codemirror/typescript/get-signature.ts +94 -0
  27. package/src/codemirror/typescript/prettier-plugin-external-typescript.vendor.js +4968 -0
  28. package/src/codemirror/typescript/runner.ts +396 -0
  29. package/src/codemirror/typescript/special-info.ts +171 -0
  30. package/src/codemirror/typescript/worker.ts +292 -0
  31. package/src/codemirror/typescript.tsx +198 -0
  32. package/src/create.tsx +44 -0
  33. package/src/icon.tsx +21 -0
  34. package/src/index.ts +6 -0
  35. package/src/logs-context.ts +5 -0
  36. package/src/playground.css +359 -0
  37. package/src/sandbox-worker/in-frame.js +179 -0
  38. package/src/sandbox-worker/index.ts +202 -0
  39. package/src/use-storage.ts +54 -0
  40. package/src/util.ts +29 -0
  41. package/src/virtual-module.d.ts +45 -0
  42. package/src/vite-env.d.ts +1 -0
  43. package/test/get-signature.test.ts +73 -0
  44. package/test/use-storage.test.ts +60 -0
  45. package/tsconfig.json +11 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # @stainless-api/playgrounds
2
+
3
+ ## 0.0.1-beta.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 2e1639a: prep for release
8
+ - Updated dependencies [239e28c]
9
+ - Updated dependencies [2e1639a]
10
+ - Updated dependencies [ddc4593]
11
+ - Updated dependencies [2e1639a]
12
+ - Updated dependencies [2e1639a]
13
+ - @stainless-api/docs-ui@0.1.0-beta.44
14
+ - @stainless/sdk-json@0.1.0-beta.1
15
+ - @stainless-api/ui-primitives@0.1.0-beta.33
package/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # `@stainless-api/playgrounds`
2
+
3
+ This is a plugin for `@stainless-api/docs` that adds a "Play" button to code snippets, allowing users to edit and run them in their browser.
4
+
5
+ ## Usage
6
+
7
+ ```diff
8
+ + import playgrounds from '@stainless-api/playgrounds';
9
+ // ...
10
+ export default defineConfig({
11
+ integrations: [
12
+ stainlessDocs({
13
+ apiReference: {
14
+ + experimentalPlaygrounds: playgrounds,
15
+ },
16
+ }),
17
+ ],
18
+ });
19
+ ```
20
+
21
+ ## Notes
22
+
23
+ If you have a Python SDK you will need python3 installed (and python3-virtualenv on Debian)
@@ -0,0 +1,2 @@
1
+ import { config } from '@stainless/eslint-config/astro';
2
+ export default config;
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@stainless-api/playgrounds",
3
+ "version": "0.0.1-beta.0",
4
+ "type": "module",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "exports": {
9
+ ".": "./src/index.ts",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "peerDependencies": {
13
+ "react": ">=19.0.0",
14
+ "react-dom": ">=19.0.0",
15
+ "vite": ">=6.2.1"
16
+ },
17
+ "dependencies": {
18
+ "@codemirror/autocomplete": "^6.18.7",
19
+ "@codemirror/commands": "^6.8.1",
20
+ "@codemirror/lang-javascript": "^6.2.4",
21
+ "@codemirror/lang-python": "^6.2.1",
22
+ "@codemirror/language": "^6.11.3",
23
+ "@codemirror/legacy-modes": "^6.5.1",
24
+ "@codemirror/lint": "^6.8.5",
25
+ "@codemirror/lsp-client": "^6.1.2",
26
+ "@codemirror/search": "^6.5.11",
27
+ "@codemirror/state": "^6.5.2",
28
+ "@codemirror/theme-one-dark": "^6.1.3",
29
+ "@codemirror/view": "^6.38.2",
30
+ "@preact/signals-core": "^1.12.1",
31
+ "@replit/codemirror-vscode-keymap": "^6.0.2",
32
+ "@shikijs/types": "^3.15.0",
33
+ "@stainless-api/codemirror-ts": "^3.0.1",
34
+ "@typescript/vfs": "^1.6.1",
35
+ "clsx": "^2.1.1",
36
+ "corepack": "^0.34.3",
37
+ "dompurify": "^3.2.6",
38
+ "js-tokens": "^9.0.1",
39
+ "lines-and-columns": "^2.0.4",
40
+ "lucide-react": "^0.555.0",
41
+ "marked": "^16.0.0",
42
+ "shiki": "^3.19.0",
43
+ "source-map": "^0.7.6",
44
+ "type-fest": "^5.3.0",
45
+ "unenv": "^1.10.0",
46
+ "vite-plugin-prebundle-workers": "^0.2.0",
47
+ "vscode-languageserver-protocol": "^3.17.5",
48
+ "@stainless-api/docs-ui": "0.1.0-beta.44",
49
+ "@stainless-api/ui-primitives": "0.1.0-beta.33",
50
+ "@stainless/sdk-json": "^0.1.0-beta.1"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "24.10.1",
54
+ "@types/react": "19.2.7",
55
+ "@types/react-dom": "^19.2.3",
56
+ "is-callable": "^1.2.7",
57
+ "pyodide": "^0.28.2",
58
+ "react": "^19.2.1",
59
+ "react-dom": "^19.2.1",
60
+ "typescript": "5.9.3",
61
+ "vite": "^6.4.1",
62
+ "@stainless/eslint-config": "0.1.0-beta.0"
63
+ },
64
+ "scripts": {
65
+ "lint": "eslint .",
66
+ "check:types": "tsc --noEmit",
67
+ "test": "node --test"
68
+ }
69
+ }
package/src/Logs.tsx ADDED
@@ -0,0 +1,216 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useEffect,
5
+ useLayoutEffect,
6
+ useRef,
7
+ useState,
8
+ useSyncExternalStore,
9
+ type PropsWithChildren,
10
+ } from 'react';
11
+ import { effect } from '@preact/signals-core';
12
+ import style from '@stainless-api/docs-ui/style';
13
+ import { Bug, ChevronRight, CircleAlert, Info, LogsIcon, Terminal, Trash, TriangleAlert } from 'lucide-react';
14
+ import { LogsContext } from './logs-context';
15
+ import { Panel } from './Panel';
16
+ import { Button } from '@stainless-api/ui-primitives';
17
+
18
+ export type JSLogType =
19
+ | 'error'
20
+ | 'warn'
21
+ | 'log'
22
+ | 'info'
23
+ | 'debug'
24
+ | 'dir'
25
+ | 'trace'
26
+ | 'group'
27
+ | 'groupCollapsed'
28
+ | 'groupEnd';
29
+ export type LogType = JSLogType | 'stdout' | 'stderr';
30
+ export type Log =
31
+ | {
32
+ type: LogType;
33
+ parts: (string | Part)[];
34
+ }
35
+ | { type: 'clear'; parts?: undefined };
36
+ export type Logger = (log: Log) => void;
37
+ export type Part = {
38
+ css: string | undefined;
39
+ value: string | Part[];
40
+ expandHandle?: string;
41
+ id?: string;
42
+ lowPriority?: boolean;
43
+ };
44
+ export type StateLog =
45
+ | {
46
+ type: LogType;
47
+ parts: (string | StatePart)[];
48
+ }
49
+ | { type: 'clear'; parts?: undefined };
50
+ export type StatePart = {
51
+ css: string | undefined;
52
+ value: string | StatePart[];
53
+ handle?: { expand(): Promise<StatePart[]>; free(): void; id?: string };
54
+ lowPriority?: boolean;
55
+ };
56
+
57
+ function InlineStyled({ style, children }: PropsWithChildren<{ style: string | undefined }>) {
58
+ const ref = useRef<HTMLSpanElement>(null);
59
+ useLayoutEffect(() => {
60
+ if (style === undefined) {
61
+ ref.current?.removeAttribute('style');
62
+ } else {
63
+ ref.current?.setAttribute('style', style);
64
+ }
65
+ }, [style]);
66
+ return <span ref={ref}>{children}</span>;
67
+ }
68
+ const ParentsContext = createContext<(string | undefined)[]>([]);
69
+ const SiblingsContext = createContext<number>(0);
70
+ function RenderPart({ part }: { part: StatePart }) {
71
+ const parents = useContext(ParentsContext);
72
+ const siblings = useContext(SiblingsContext);
73
+ const [open, setOpen] = useState(
74
+ !part.lowPriority &&
75
+ (!part.handle?.id || !parents.includes(part.handle?.id)) &&
76
+ parents.length < 5 &&
77
+ siblings < 10,
78
+ );
79
+ const [children, setChildren] = useState<StatePart[]>();
80
+ useEffect(() => {
81
+ return () => children?.forEach((e) => e.handle?.free());
82
+ }, [children]);
83
+ useLayoutEffect(() => {
84
+ if (open) {
85
+ part.handle?.expand().then((e) => {
86
+ if (children?.length && !e.length) return;
87
+ setChildren(e);
88
+ });
89
+ }
90
+ }, [open]);
91
+ const value =
92
+ typeof part.value === 'string'
93
+ ? part.value
94
+ : part.value.map((part, i) => <RenderPart key={i} part={part} />);
95
+ return (
96
+ <ParentsContext value={[...parents, part.handle?.id]}>
97
+ <InlineStyled style={part.css}>
98
+ {part.handle === undefined ? (
99
+ value
100
+ ) : (
101
+ <>
102
+ <button
103
+ className={
104
+ 'playground-logs-object-toggle' + (open ? ' playground-logs-object-toggle-open' : '')
105
+ }
106
+ title={open ? 'Collapse' : 'Open'}
107
+ type="button"
108
+ onClick={() => {
109
+ setOpen(!open);
110
+ }}
111
+ >
112
+ <ChevronRight className={style.Icon} size={16} />
113
+ </button>
114
+ <span
115
+ onClick={() => {
116
+ setOpen(!open);
117
+ }}
118
+ >
119
+ {value}
120
+ </span>
121
+ </>
122
+ )}
123
+ </InlineStyled>
124
+ {open && children ? (
125
+ <div className="playground-logs-object-properties">
126
+ <SiblingsContext value={children.length}>
127
+ {children.map((e, i) => (
128
+ <RenderPart part={e} key={i} />
129
+ ))}
130
+ </SiblingsContext>
131
+ </div>
132
+ ) : null}
133
+ </ParentsContext>
134
+ );
135
+ }
136
+ function LogContent({ content: { type, parts } }: { content: StateLog }) {
137
+ const Icon =
138
+ type === 'debug'
139
+ ? Bug
140
+ : type === 'error'
141
+ ? CircleAlert
142
+ : type === 'info'
143
+ ? Info
144
+ : type === 'warn'
145
+ ? TriangleAlert
146
+ : 'span';
147
+ return (
148
+ <li className={'playground-log-' + type}>
149
+ <Icon className={style.Icon} />{' '}
150
+ <span className={'playground-logs-content'}>
151
+ {type === 'clear'
152
+ ? 'Logs were cleared.'
153
+ : parts.map((part, i) => (typeof part === 'string' ? part : <RenderPart key={i} part={part} />))}
154
+ </span>
155
+ </li>
156
+ );
157
+ }
158
+
159
+ export function Logs() {
160
+ const logsSignal = useContext(LogsContext)!;
161
+ const logLines = useSyncExternalStore<StateLog[]>(
162
+ (onChange) => {
163
+ let changeQueued;
164
+ return effect(
165
+ () => (
166
+ logsSignal.valueOf(),
167
+ (changeQueued ||= setTimeout(() => {
168
+ onChange();
169
+ changeQueued = null;
170
+ }, 10))
171
+ ),
172
+ );
173
+ },
174
+ () => logsSignal.peek(),
175
+ );
176
+ const logsRef = useRef<HTMLUListElement>(null);
177
+ useEffect(() => {
178
+ return clearTimeout.bind(
179
+ null,
180
+ setTimeout(() => {
181
+ const logs = logsRef.current!;
182
+ logs.scrollTo({
183
+ top: logs.scrollHeight,
184
+ behavior: 'smooth',
185
+ });
186
+ }, 100),
187
+ );
188
+ }, [logLines]);
189
+ return (
190
+ <Panel
191
+ title="Logs"
192
+ icon={LogsIcon}
193
+ actions={
194
+ <Button
195
+ onClick={() => (logsSignal.value = [])}
196
+ aria-label="Clear Logs"
197
+ title="Clear Logs"
198
+ variant="outline"
199
+ >
200
+ <Trash size={16} className={style.Icon} />
201
+ </Button>
202
+ }
203
+ >
204
+ <ul ref={logsRef} className="playground-logs playground-panel-content">
205
+ {logLines.length ? (
206
+ logLines.map((e, i) => <LogContent key={i} content={e} />)
207
+ ) : (
208
+ <li className="playground-logs-blankslate">
209
+ <Terminal />
210
+ <span>No logs yet!</span>
211
+ </li>
212
+ )}
213
+ </ul>
214
+ </Panel>
215
+ );
216
+ }
package/src/Panel.tsx ADDED
@@ -0,0 +1,21 @@
1
+ import { type PropsWithChildren } from 'react';
2
+ import style from '@stainless-api/docs-ui/style';
3
+ import { LucideIcon } from 'lucide-react';
4
+
5
+ export function Panel({
6
+ title,
7
+ icon: Icon,
8
+ children,
9
+ actions,
10
+ }: PropsWithChildren<{ title: string; icon: LucideIcon; actions?: React.ReactNode }>) {
11
+ return (
12
+ <div className="playground-panel">
13
+ <div className="playground-panel-header">
14
+ <Icon size={16} className={style.Icon} />
15
+ <h5 className="playground-panel-title">{title}</h5>
16
+ {actions}
17
+ </div>
18
+ {children}
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,5 @@
1
+ import { PropsWithChildren } from 'react';
2
+
3
+ export function PlaygroundPanelWrapper({ children }: PropsWithChildren) {
4
+ return <div className="playground-panel-wrapper">{children}</div>;
5
+ }
@@ -0,0 +1,152 @@
1
+ import { promises as fs, globSync } from 'fs';
2
+ import path, { dirname, resolve } from 'path';
3
+ import os from 'os';
4
+ import { glob, readdir, realpath, writeFile } from 'fs/promises';
5
+ import type data from 'virtual:stl-playground/python.json';
6
+ import { execCommand } from './util';
7
+ import { createRequire } from 'module';
8
+
9
+ export async function buildPythonTypes(typesPath: string, installPackage: string) {
10
+ const tmpDir = await realpath(await fs.mkdtemp(path.join(os.tmpdir(), 'py-types-')));
11
+
12
+ try {
13
+ const pyrightPromise = (async () => {
14
+ await fs.writeFile(path.join(tmpDir, 'package.json'), '{}');
15
+ await execCommand(
16
+ process.execPath,
17
+ [
18
+ resolve(dirname(createRequire(import.meta.url).resolve('corepack/package.json')), 'dist/pnpm.js'),
19
+ 'install',
20
+ 'pyright',
21
+ ],
22
+ {
23
+ cwd: tmpDir,
24
+ },
25
+ );
26
+ return { pyright: path.join(tmpDir, 'node_modules', '.bin', 'pyright') };
27
+ })();
28
+ const venvPromise = (async () => {
29
+ try {
30
+ await execCommand('python3', ['-m', 'venv', '.'], { cwd: tmpDir });
31
+ } catch (e) {
32
+ throw new Error(
33
+ 'Failed to set up virtual environment for playground build. Do you have Python installed?',
34
+ {
35
+ cause: e,
36
+ },
37
+ );
38
+ }
39
+ const venvBin = path.join(tmpDir, 'bin');
40
+ const pip = path.join(venvBin, 'pip');
41
+ return { pip };
42
+ })();
43
+ const initialWheelsDirPromise = (async () => {
44
+ const initialWheelsDir = path.join(tmpDir, 'wheels');
45
+ await fs.mkdir(initialWheelsDir);
46
+ return { initialWheelsDir };
47
+ })();
48
+ const pkgsDirPromise = (async () => {
49
+ const pkgsDir = path.join(tmpDir, 'pkgs');
50
+ await fs.mkdir(pkgsDir);
51
+ return { pkgsDir };
52
+ })();
53
+ const wheelPathPromise = (async () => {
54
+ const [{ initialWheelsDir }, { pip }] = await Promise.all([initialWheelsDirPromise, venvPromise]);
55
+ await execCommand(
56
+ pip,
57
+ ['wheel', '--no-deps', '-w', 'wheels', installPackage.replace(/^pip install /, '')],
58
+ {
59
+ cwd: tmpDir,
60
+ },
61
+ );
62
+ const wheelPath = (await readdir(initialWheelsDir))[0]!;
63
+ return { wheelPath, wheelsDir: initialWheelsDir };
64
+ })();
65
+ const richPromise = (async () => {
66
+ const response = await fetch(
67
+ 'https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl',
68
+ );
69
+ if (!response.ok) {
70
+ throw new Error(`Failed to download wheel: ${response.statusText}`);
71
+ }
72
+ const { wheelsDir } = await wheelPathPromise;
73
+ writeFile(path.join(wheelsDir, 'rich-14.1.0-py3-none-any.whl'), await response.bytes());
74
+ })();
75
+ const packagesPromise = (async () => {
76
+ const [{ pip }] = await Promise.all([venvPromise, pkgsDirPromise, richPromise, wheelPathPromise]);
77
+
78
+ await execCommand(
79
+ pip,
80
+ [
81
+ 'install',
82
+ '--target',
83
+ 'pkgs',
84
+ ...globSync('wheels/*', {
85
+ cwd: tmpDir,
86
+ }),
87
+ ],
88
+ { cwd: tmpDir },
89
+ );
90
+
91
+ const packages = globSync('pkgs/*/__init__.py', { cwd: tmpDir }).map((p) =>
92
+ path.basename(path.dirname(p)),
93
+ );
94
+ return { packages };
95
+ })();
96
+
97
+ const [{ packages }, { pyright }, { wheelsDir, wheelPath }] = await Promise.all([
98
+ packagesPromise,
99
+ pyrightPromise,
100
+ wheelPathPromise,
101
+ ]);
102
+
103
+ await Promise.all(
104
+ packages.map((pkg) =>
105
+ execCommand(pyright, ['--createstub', pkg], {
106
+ cwd: tmpDir,
107
+ env: { PYTHONPATH: 'pkgs' },
108
+ }),
109
+ ),
110
+ );
111
+
112
+ const typingsDir = path.join(tmpDir, 'typings');
113
+ const files: Record<string, string> = {};
114
+ const readPromises: Promise<void>[] = [];
115
+ for await (const file of glob('**/*', { cwd: typingsDir, withFileTypes: true })) {
116
+ if (!(file.isFile() || file.isSymbolicLink())) continue;
117
+ const readPromise = (async () => {
118
+ const fullPath = path.join(file.parentPath, file.name);
119
+ const content = await fs.readFile(fullPath, 'utf-8');
120
+ const cleanedContent = content.replace(
121
+ '"""\nThis type stub file was generated by pyright.\n"""\n',
122
+ '',
123
+ );
124
+ const targetPath = `/play/.venv/lib/site-packages/${path.relative(typingsDir, fullPath)}`;
125
+ files[targetPath] = cleanedContent;
126
+ })();
127
+ readPromises.push(readPromise);
128
+ readPromise.catch(() => {});
129
+ }
130
+
131
+ await Promise.all([
132
+ ...readPromises,
133
+ (async () => {
134
+ await fs.rm(path.join(typesPath, 'python'), { recursive: true, force: true });
135
+ await fs.mkdir(path.join(typesPath, 'python'));
136
+ })(),
137
+ ]);
138
+
139
+ await Promise.all([
140
+ fs.copyFile(path.join(wheelsDir, wheelPath), path.join(typesPath, 'python', 'wheel.whl')),
141
+ fs.writeFile(
142
+ path.join(typesPath, 'python.json'),
143
+ JSON.stringify({
144
+ files: files,
145
+ wheel: wheelPath,
146
+ } satisfies typeof data),
147
+ ),
148
+ ]);
149
+ } finally {
150
+ await fs.rm(tmpDir, { recursive: true, force: true });
151
+ }
152
+ }
@@ -0,0 +1,70 @@
1
+ import { promises as fs } from 'fs';
2
+ import { readdirSync, readFileSync, realpathSync, statSync } from 'fs';
3
+ import path, { dirname, resolve } from 'path';
4
+ import os from 'os';
5
+ import { createRequire } from 'module';
6
+ import { execCommand } from './util';
7
+
8
+ export async function buildTypescriptTypes(typesPath: string, installCommand: string) {
9
+ const tmpDir = realpathSync(await fs.mkdtemp(path.join(os.tmpdir(), 'ts-types-')));
10
+
11
+ try {
12
+ await fs.writeFile(path.join(tmpDir, 'package.json'), '{}');
13
+ await execCommand(
14
+ process.execPath,
15
+ [
16
+ resolve(dirname(createRequire(import.meta.url).resolve('corepack/package.json')), 'dist/pnpm.js'),
17
+ 'install',
18
+ installCommand.replace(/^npm install /, ''),
19
+ 'typescript',
20
+ ],
21
+ {
22
+ cwd: tmpDir,
23
+ },
24
+ );
25
+
26
+ const links: [string, string][] = [];
27
+ const files: [string, string][] = [];
28
+
29
+ function* walkSync(dir: string, seen = new Set<string>()): Generator<[string, string]> {
30
+ for (const file of readdirSync(dir)) {
31
+ const filepath = path.join(dir, file);
32
+ const real = realpathSync(filepath);
33
+ const stats = statSync(real);
34
+
35
+ if (real !== path.resolve(filepath)) {
36
+ links.push(['/' + path.relative(tmpDir, filepath), '/' + path.relative(tmpDir, real)]);
37
+ }
38
+
39
+ if (seen.has(real)) {
40
+ return;
41
+ } else {
42
+ seen.add(real);
43
+ }
44
+
45
+ if (stats.isDirectory()) {
46
+ yield* walkSync(real, seen);
47
+ } else if (stats.isFile() && /\.d\.[cm]?ts(\.map)?$|[/\\]package\.json$/.test(filepath)) {
48
+ const rel = path.relative(tmpDir, path.resolve(filepath));
49
+ const lib = /^.+\/typescript\/lib\/lib/;
50
+
51
+ if (lib.test(rel)) {
52
+ links.push(['/' + rel.replace(lib, 'lib'), '/' + rel]);
53
+ }
54
+
55
+ yield ['/' + rel, readFileSync(filepath, 'utf-8')];
56
+ }
57
+ }
58
+ }
59
+
60
+ const nodeModulesPath = path.join(tmpDir, 'node_modules');
61
+ for (const file of walkSync(nodeModulesPath)) {
62
+ files.push(file);
63
+ }
64
+
65
+ const output = JSON.stringify({ files, links });
66
+ await fs.writeFile(path.join(typesPath, 'typescript.json'), output);
67
+ } finally {
68
+ await fs.rm(tmpDir, { recursive: true, force: true });
69
+ }
70
+ }
package/src/build.ts ADDED
@@ -0,0 +1,97 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { buildTypescriptTypes } from './build-ts-types';
3
+ import { buildPythonTypes } from './build-py-types';
4
+ import path from 'node:path';
5
+ import type * as SDKJSON from '@stainless/sdk-json';
6
+
7
+ export type Auth = Array<{
8
+ type: 'http_bearer' | 'query' | 'header' | 'oauth2' | 'http_basic' | 'http_digest';
9
+ description?: string;
10
+ name: string;
11
+ title: string;
12
+ header: string | undefined;
13
+ example: string | undefined;
14
+ opts: {
15
+ type: 'string' | 'number' | 'boolean' | 'null' | 'integer';
16
+ nullable: boolean;
17
+ description?: string | undefined;
18
+ example?: unknown;
19
+ default?: unknown;
20
+ read_env?: string | undefined;
21
+ auth?:
22
+ | {
23
+ security_scheme: string;
24
+ role?: 'value' | 'password' | 'username' | 'client_id' | 'client_secret' | undefined;
25
+ }
26
+ | undefined;
27
+ name: string;
28
+ }[];
29
+ }>;
30
+
31
+ export async function buildPlaygrounds({
32
+ spec,
33
+ langs,
34
+ auth,
35
+ playPath,
36
+ reportError,
37
+ }: {
38
+ spec: SDKJSON.Spec;
39
+ langs: SDKJSON.SpecLanguage[];
40
+ auth: Auth;
41
+ playPath: string;
42
+ reportError: (message: string) => void;
43
+ }) {
44
+ const cacheKey = JSON.stringify([
45
+ 1,
46
+ langs,
47
+ Object.entries(spec.metadata).map(([k, v]) => [k, v.install, v.version]),
48
+ auth,
49
+ ]);
50
+ const { cachedKey } = await (async () => {
51
+ await mkdir(playPath, { recursive: true });
52
+ const cachedKey = await readFile(path.join(playPath, 'cache-key'), 'utf-8').catch(() => {});
53
+ return { cachedKey };
54
+ })();
55
+ if (cacheKey === cachedKey) return;
56
+
57
+ let softFail = false;
58
+ await Promise.all([
59
+ (async () => {
60
+ try {
61
+ const tsInstall = spec.metadata.typescript?.install;
62
+ if (tsInstall && langs.includes('typescript')) {
63
+ return await buildTypescriptTypes(playPath, tsInstall);
64
+ }
65
+ } catch {
66
+ reportError('Playgrounds build for TypeScript failed! TypeScript playgrounds will not be available.');
67
+ softFail = true;
68
+ }
69
+ await writeFile(path.join(playPath, 'typescript.json'), 'null');
70
+ })(),
71
+ (async () => {
72
+ try {
73
+ const pyInstall = spec.metadata.python?.install;
74
+ if (pyInstall && langs.includes('python')) {
75
+ return await buildPythonTypes(playPath, pyInstall);
76
+ }
77
+ } catch {
78
+ reportError(
79
+ 'Playgrounds build for Python failed! Python playgrounds will not be available. Do you have python3 and python3-virtualenv installed?',
80
+ );
81
+ softFail = true;
82
+ }
83
+ await writeFile(path.join(playPath, 'python.json'), 'null');
84
+ await mkdir(path.join(playPath, 'python'), { recursive: true });
85
+ await writeFile(path.join(playPath, 'python', 'wheel.whl'), '');
86
+ })(),
87
+ writeFile(path.join(playPath, 'auth.json'), JSON.stringify(auth)),
88
+ ]).then(async () => {
89
+ if (softFail) {
90
+ // don't cache failed builds
91
+ // todo: partial caching
92
+ await rm(path.join(playPath, 'cache-key')).catch(() => {});
93
+ } else {
94
+ await writeFile(path.join(playPath, 'cache-key'), cacheKey, 'utf-8');
95
+ }
96
+ });
97
+ }