@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.
- package/CHANGELOG.md +15 -0
- package/README.md +23 -0
- package/eslint.config.js +2 -0
- package/package.json +69 -0
- package/src/Logs.tsx +216 -0
- package/src/Panel.tsx +21 -0
- package/src/PlaygroundPanelWrapper.tsx +5 -0
- package/src/build-py-types.ts +152 -0
- package/src/build-ts-types.ts +70 -0
- package/src/build.ts +97 -0
- package/src/codemirror/comlink.ts +698 -0
- package/src/codemirror/curl/curlconverter.vendor.js +7959 -0
- package/src/codemirror/curl.ts +108 -0
- package/src/codemirror/deps.ts +12 -0
- package/src/codemirror/fix-lsp-markdown.ts +50 -0
- package/src/codemirror/lsp.ts +87 -0
- package/src/codemirror/python/anser.ts +398 -0
- package/src/codemirror/python/pyodide.ts +180 -0
- package/src/codemirror/python.ts +160 -0
- package/src/codemirror/react.tsx +615 -0
- package/src/codemirror/sanitize-html.ts +12 -0
- package/src/codemirror/shiki.ts +65 -0
- package/src/codemirror/typescript/cdn-typescript.d.ts +1 -0
- package/src/codemirror/typescript/cdn-typescript.js +1 -0
- package/src/codemirror/typescript/console.ts +590 -0
- package/src/codemirror/typescript/get-signature.ts +94 -0
- package/src/codemirror/typescript/prettier-plugin-external-typescript.vendor.js +4968 -0
- package/src/codemirror/typescript/runner.ts +396 -0
- package/src/codemirror/typescript/special-info.ts +171 -0
- package/src/codemirror/typescript/worker.ts +292 -0
- package/src/codemirror/typescript.tsx +198 -0
- package/src/create.tsx +44 -0
- package/src/icon.tsx +21 -0
- package/src/index.ts +6 -0
- package/src/logs-context.ts +5 -0
- package/src/playground.css +359 -0
- package/src/sandbox-worker/in-frame.js +179 -0
- package/src/sandbox-worker/index.ts +202 -0
- package/src/use-storage.ts +54 -0
- package/src/util.ts +29 -0
- package/src/virtual-module.d.ts +45 -0
- package/src/vite-env.d.ts +1 -0
- package/test/get-signature.test.ts +73 -0
- package/test/use-storage.test.ts +60 -0
- 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)
|
package/eslint.config.js
ADDED
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,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
|
+
}
|