@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
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { createVirtualTypeScriptEnvironment, VirtualTypeScriptEnvironment } from '@typescript/vfs';
|
|
2
|
+
import ts from './cdn-typescript';
|
|
3
|
+
import * as Comlink from '../comlink';
|
|
4
|
+
import { createWorker, HoverInfo } from '@stainless-api/codemirror-ts/worker';
|
|
5
|
+
import { format } from 'prettier';
|
|
6
|
+
import { parsers } from './prettier-plugin-external-typescript.vendor';
|
|
7
|
+
import estree from 'prettier/plugins/estree';
|
|
8
|
+
import { SourceMapConsumer } from 'source-map';
|
|
9
|
+
import { LinesAndColumns } from 'lines-and-columns';
|
|
10
|
+
|
|
11
|
+
type TypescriptTypes = NonNullable<typeof import('virtual:stl-playground/typescript.json').default>;
|
|
12
|
+
|
|
13
|
+
// @ts-expect-error this is kinda awful but it makes ts autocomplete work without source patching
|
|
14
|
+
Object.prototype.keepLegacyLimitationForAutocompletionSymbols = false;
|
|
15
|
+
|
|
16
|
+
function escapeRegExp(text: string) {
|
|
17
|
+
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
|
18
|
+
}
|
|
19
|
+
let realpathRe;
|
|
20
|
+
const realpath = (typescriptTypes: TypescriptTypes, path: string) => {
|
|
21
|
+
realpathRe ??= new RegExp(
|
|
22
|
+
`^(${typescriptTypes!.links.map((e, i) => `(?<_${i}>${escapeRegExp(e[0])})`).join('|')})(/|$)`,
|
|
23
|
+
);
|
|
24
|
+
let match;
|
|
25
|
+
while ((match = path.match(realpathRe))) {
|
|
26
|
+
const entry =
|
|
27
|
+
typescriptTypes!.links[
|
|
28
|
+
Object.entries(match.groups!)
|
|
29
|
+
.find((e) => e[1])![0]
|
|
30
|
+
.slice(1) as `${number}`
|
|
31
|
+
]!;
|
|
32
|
+
path = entry[1] + (match[2 + typescriptTypes!.links.length] ?? '') + path.slice(match[0].length);
|
|
33
|
+
}
|
|
34
|
+
return path;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function notImplemented(methodName: string): never {
|
|
38
|
+
throw new Error(`Method '${methodName}' is not implemented.`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function audit<ArgsT extends unknown[], ReturnT>(
|
|
42
|
+
_name: string,
|
|
43
|
+
fn: (...args: ArgsT) => ReturnT,
|
|
44
|
+
): (...args: ArgsT) => ReturnT {
|
|
45
|
+
return (...args) => {
|
|
46
|
+
const res = fn(...args);
|
|
47
|
+
|
|
48
|
+
//const smallRes = typeof res === "string" ? res.slice(0, 80) + "..." : res;
|
|
49
|
+
//debugLog("> " + name, ...args);
|
|
50
|
+
//debugLog("< " + smallRes);
|
|
51
|
+
|
|
52
|
+
return res;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// "/DOM.d.ts" => "/lib.dom.d.ts"
|
|
57
|
+
const libize = (path: string) => path.replace('/', '/lib.').toLowerCase();
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates an in-memory System object which can be used in a TypeScript program, this
|
|
61
|
+
* is what provides read/write aspects of the virtual fs
|
|
62
|
+
*/
|
|
63
|
+
export function createSystem(typescriptTypes: TypescriptTypes, files: Map<string, string>): ts.System {
|
|
64
|
+
return {
|
|
65
|
+
args: [],
|
|
66
|
+
createDirectory: () => notImplemented('createDirectory'),
|
|
67
|
+
// TODO: could make a real file tree
|
|
68
|
+
directoryExists: audit('directoryExists', (directory) => {
|
|
69
|
+
directory = realpath(typescriptTypes, directory);
|
|
70
|
+
return Array.from(files.keys()).some((path) => path.startsWith(directory));
|
|
71
|
+
}),
|
|
72
|
+
exit: () => notImplemented('exit'),
|
|
73
|
+
fileExists: audit(
|
|
74
|
+
'fileExists',
|
|
75
|
+
(fileName) =>
|
|
76
|
+
files.has(realpath(typescriptTypes, fileName)) ||
|
|
77
|
+
files.has(realpath(typescriptTypes, libize(fileName))),
|
|
78
|
+
),
|
|
79
|
+
getCurrentDirectory: () => '/',
|
|
80
|
+
getDirectories: () => [],
|
|
81
|
+
getExecutingFilePath: () => notImplemented('getExecutingFilePath'),
|
|
82
|
+
readDirectory: audit('readDirectory', (directory) => (directory === '/' ? Array.from(files.keys()) : [])),
|
|
83
|
+
readFile: audit(
|
|
84
|
+
'readFile',
|
|
85
|
+
(fileName) =>
|
|
86
|
+
files.get(realpath(typescriptTypes, fileName)) ??
|
|
87
|
+
files.get(realpath(typescriptTypes, libize(fileName))),
|
|
88
|
+
),
|
|
89
|
+
resolvePath: (path) => realpath(typescriptTypes, path),
|
|
90
|
+
newLine: '\n',
|
|
91
|
+
useCaseSensitiveFileNames: true,
|
|
92
|
+
write: () => notImplemented('write'),
|
|
93
|
+
writeFile: (fileName, contents) => {
|
|
94
|
+
files.set(realpath(typescriptTypes, fileName), contents);
|
|
95
|
+
},
|
|
96
|
+
deleteFile: (fileName) => {
|
|
97
|
+
files.delete(realpath(typescriptTypes, fileName));
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type PromiseWithResolvers<T> = {
|
|
103
|
+
promise: Promise<T>;
|
|
104
|
+
resolve: (value: T) => void;
|
|
105
|
+
reject: (reason?: unknown) => void;
|
|
106
|
+
};
|
|
107
|
+
function promiseWithResolvers<T>(): PromiseWithResolvers<T> {
|
|
108
|
+
let resolve, reject;
|
|
109
|
+
const promise = new Promise<T>((_resolve, _reject) => {
|
|
110
|
+
resolve = _resolve;
|
|
111
|
+
reject = _reject;
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
promise,
|
|
115
|
+
resolve: resolve!,
|
|
116
|
+
reject: reject!,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const compilerOpts: ts.CompilerOptions = {
|
|
121
|
+
module: ts.ModuleKind.NodeNext,
|
|
122
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
123
|
+
lib: ['esnext', 'webworker', 'webworker.iterable'],
|
|
124
|
+
types: ['stl-play'],
|
|
125
|
+
target: ts.ScriptTarget.ESNext,
|
|
126
|
+
moduleDetection: ts.ModuleDetectionKind.Force,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const initData = promiseWithResolvers<{
|
|
130
|
+
suggestedEnv: string[];
|
|
131
|
+
typescriptTypes: TypescriptTypes;
|
|
132
|
+
}>();
|
|
133
|
+
|
|
134
|
+
let typescriptTypes: TypescriptTypes;
|
|
135
|
+
let fsMap: Map<string, string>;
|
|
136
|
+
type RawCompletionItem = {
|
|
137
|
+
label: string;
|
|
138
|
+
type: Completion['type'];
|
|
139
|
+
} & Pick<ts.CompletionEntryDetails, 'codeActions' | 'displayParts' | 'documentation' | 'tags'>;
|
|
140
|
+
type RawCompletion = {
|
|
141
|
+
from: number;
|
|
142
|
+
options: RawCompletionItem[];
|
|
143
|
+
};
|
|
144
|
+
const worker: {
|
|
145
|
+
initialize(): Promise<void>;
|
|
146
|
+
updateFile({ path, code }: { path: string; code: string }): void;
|
|
147
|
+
getLints({ path }: { path: string }): Diagnostic[];
|
|
148
|
+
getAutocompletion({
|
|
149
|
+
path,
|
|
150
|
+
context,
|
|
151
|
+
}: {
|
|
152
|
+
path: string;
|
|
153
|
+
context: Pick<CompletionContext, 'pos' | 'explicit'>;
|
|
154
|
+
}): Promise<RawCompletion | null> | null;
|
|
155
|
+
getHover({ path, pos }: { path: string; pos: number }): HoverInfo | null;
|
|
156
|
+
getEnv(): VirtualTypeScriptEnvironment;
|
|
157
|
+
} = createWorker({
|
|
158
|
+
env: (async () => {
|
|
159
|
+
const { typescriptTypes, suggestedEnv } = await initData.promise;
|
|
160
|
+
fsMap = new Map<string, string>(typescriptTypes.files as [string, string][]);
|
|
161
|
+
fsMap.set(
|
|
162
|
+
'/node_modules/@types/stl-play/package.json',
|
|
163
|
+
JSON.stringify({
|
|
164
|
+
name: '@types/stl-play',
|
|
165
|
+
types: 'index.d.ts',
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
fsMap.set(
|
|
169
|
+
'/node_modules/@types/stl-play/index.d.ts',
|
|
170
|
+
`declare global {
|
|
171
|
+
declare var process: NodeJS.Process;
|
|
172
|
+
declare namespace NodeJS {
|
|
173
|
+
interface Dict<T> {
|
|
174
|
+
[key: string]: T | undefined;
|
|
175
|
+
}
|
|
176
|
+
interface ProcessEnv extends Dict<string> {\n${['TZ', ...suggestedEnv].map((e) => ` ${JSON.stringify(e)}?: string | undefined;\n`)} }
|
|
177
|
+
interface Process {
|
|
178
|
+
/**
|
|
179
|
+
* The \`process.env\` property returns an object containing environment variables.
|
|
180
|
+
*/
|
|
181
|
+
env: ProcessEnv;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export {}`,
|
|
186
|
+
);
|
|
187
|
+
const system = createSystem(typescriptTypes, fsMap);
|
|
188
|
+
return createVirtualTypeScriptEnvironment(system, [], ts, compilerOpts);
|
|
189
|
+
})(),
|
|
190
|
+
});
|
|
191
|
+
const api = {
|
|
192
|
+
...worker,
|
|
193
|
+
async getHover(opts: { path: string; pos: number }): Promise<HoverInfo | null> {
|
|
194
|
+
const result = worker.getHover(opts);
|
|
195
|
+
if (result?.def) {
|
|
196
|
+
result.def = await Promise.all(result.def.map(fixDefInfo));
|
|
197
|
+
}
|
|
198
|
+
if (result?.typeDef) {
|
|
199
|
+
result.typeDef = await Promise.all(result.typeDef.map(fixDefInfo));
|
|
200
|
+
}
|
|
201
|
+
return result;
|
|
202
|
+
},
|
|
203
|
+
stlInit(initData_: Awaited<typeof initData.promise>) {
|
|
204
|
+
typescriptTypes = initData_.typescriptTypes;
|
|
205
|
+
initData.resolve(initData_);
|
|
206
|
+
},
|
|
207
|
+
async format(code: string) {
|
|
208
|
+
const result = (
|
|
209
|
+
await format(code, {
|
|
210
|
+
parser: 'typescript',
|
|
211
|
+
plugins: [{ parsers }, estree],
|
|
212
|
+
printWidth: 75,
|
|
213
|
+
singleQuote: true,
|
|
214
|
+
})
|
|
215
|
+
).replace(/\n+$/, '');
|
|
216
|
+
if (result === code) return undefined;
|
|
217
|
+
return result;
|
|
218
|
+
},
|
|
219
|
+
async transform(code: string) {
|
|
220
|
+
return ts.transpileModule(code, {
|
|
221
|
+
fileName: 'input.mts',
|
|
222
|
+
compilerOptions: {
|
|
223
|
+
...compilerOpts,
|
|
224
|
+
noCheck: true,
|
|
225
|
+
},
|
|
226
|
+
transformers: {
|
|
227
|
+
after: [
|
|
228
|
+
(ctx) => (file) =>
|
|
229
|
+
ts.visitEachChild(
|
|
230
|
+
file,
|
|
231
|
+
(child) =>
|
|
232
|
+
ts.isImportDeclaration(child)
|
|
233
|
+
? ctx.factory.updateImportDeclaration(
|
|
234
|
+
child,
|
|
235
|
+
child.modifiers,
|
|
236
|
+
child.importClause,
|
|
237
|
+
ts.isStringLiteral(child.moduleSpecifier)
|
|
238
|
+
? ctx.factory.createStringLiteral(
|
|
239
|
+
child.moduleSpecifier.text.replace(
|
|
240
|
+
/^(?!\.{0,2}\/|[a-z][a-z0-9+.-]+:)/,
|
|
241
|
+
'https://esm.sh/',
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
: child.moduleSpecifier,
|
|
245
|
+
child.attributes,
|
|
246
|
+
)
|
|
247
|
+
: child,
|
|
248
|
+
ctx,
|
|
249
|
+
),
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
}).outputText;
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
import mappingsWasm from 'source-map/lib/mappings.wasm?url&inline';
|
|
257
|
+
import { Diagnostic } from '@codemirror/lint';
|
|
258
|
+
import { Completion, CompletionContext } from '@codemirror/autocomplete';
|
|
259
|
+
// @ts-expect-error - this is untyped
|
|
260
|
+
SourceMapConsumer.initialize({ 'lib/mappings.wasm': mappingsWasm });
|
|
261
|
+
|
|
262
|
+
const mapCache = new Map<string, { tracer: SourceMapConsumer; loc: LinesAndColumns } | null>();
|
|
263
|
+
const fixDefInfo = async (def: ts.DefinitionInfo) => {
|
|
264
|
+
def.originalFileName = def.fileName.replace(/\.d\.m?ts$/, '.ts');
|
|
265
|
+
const path = realpath(typescriptTypes!, def.fileName + '.map');
|
|
266
|
+
let cached = mapCache.get(path);
|
|
267
|
+
if (cached === undefined) {
|
|
268
|
+
const dtsString = fsMap.get(realpath(typescriptTypes!, def.fileName));
|
|
269
|
+
const mapString = fsMap.get(path);
|
|
270
|
+
if (mapString && dtsString) {
|
|
271
|
+
cached = {
|
|
272
|
+
tracer: await new SourceMapConsumer(mapString),
|
|
273
|
+
loc: new LinesAndColumns(dtsString),
|
|
274
|
+
};
|
|
275
|
+
mapCache.set(path, cached);
|
|
276
|
+
} else {
|
|
277
|
+
mapCache.set(path, null);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (cached) {
|
|
281
|
+
const loc = cached.loc.locationForIndex(def.textSpan.start);
|
|
282
|
+
if (loc) {
|
|
283
|
+
const { line } = cached.tracer.originalPositionFor(loc);
|
|
284
|
+
if (line) {
|
|
285
|
+
def.originalFileName += '#L' + line;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return def;
|
|
290
|
+
};
|
|
291
|
+
export type API = typeof api;
|
|
292
|
+
Comlink.expose(api);
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { typescriptLanguage } from '@codemirror/lang-javascript';
|
|
2
|
+
import tsWorkerURL from './typescript/worker?worker&url';
|
|
3
|
+
import runnerWorkerURL from './typescript/runner?worker&url';
|
|
4
|
+
import type { HoverInfo } from '@stainless-api/codemirror-ts/worker';
|
|
5
|
+
import { tsAutocomplete, tsFacet, tsHover, tsLinter, tsSync, tsTwoslash } from '@stainless-api/codemirror-ts';
|
|
6
|
+
import * as Comlink from './comlink';
|
|
7
|
+
import { autocompletion } from '@codemirror/autocomplete';
|
|
8
|
+
import type { JSDocTagInfo, QuickInfo, SymbolDisplayPart } from 'typescript';
|
|
9
|
+
import type { Language } from './react';
|
|
10
|
+
import { proxy, type Remote } from './comlink';
|
|
11
|
+
import type { Logger } from '../Logs';
|
|
12
|
+
import type { RunnerAPI } from './typescript/runner';
|
|
13
|
+
import typescriptTypes from 'virtual:stl-playground/typescript.json';
|
|
14
|
+
import authData from 'virtual:stl-playground/auth.json';
|
|
15
|
+
import { docToHTML } from './fix-lsp-markdown';
|
|
16
|
+
import { API } from './typescript/worker';
|
|
17
|
+
import { EditorView } from '@codemirror/view';
|
|
18
|
+
|
|
19
|
+
if (!typescriptTypes) {
|
|
20
|
+
throw new Error('TypeScript playgrounds failed to build.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function displayPartsToString(displayParts: SymbolDisplayPart[] | undefined): string {
|
|
24
|
+
if (displayParts) {
|
|
25
|
+
return displayParts.map((displayPart) => displayPart.text).join('');
|
|
26
|
+
}
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function maybeBlock(md: string) {
|
|
31
|
+
return markdown(md).startsWith('<p>') ? md : '\n' + md;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function tagToString(tag: JSDocTagInfo): string {
|
|
35
|
+
let tagLabel = `*@${tag.name}*`;
|
|
36
|
+
if (tag.name === 'param' && tag.text) {
|
|
37
|
+
const [paramName, ...rest] = tag.text;
|
|
38
|
+
tagLabel += ` \`${paramName!.text}\``;
|
|
39
|
+
if (rest.length > 0) tagLabel += ` — ${maybeBlock(rest.map((r) => r.text).join(' '))}`;
|
|
40
|
+
} else if (Array.isArray(tag.text)) {
|
|
41
|
+
tagLabel += ` — ${maybeBlock(tag.text.map((r) => r.text).join(' '))}`;
|
|
42
|
+
} else if (tag.text) {
|
|
43
|
+
tagLabel += ` — ${maybeBlock(tag.text)}`;
|
|
44
|
+
}
|
|
45
|
+
return tagLabel;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const markdown = (md: string) => {
|
|
49
|
+
return docToHTML(md, 'markdown');
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const useMeta = /iPad|iPhone|iPod|Mac/.test(navigator.userAgent);
|
|
53
|
+
type GoToOptions = {
|
|
54
|
+
gotoHandler?: (currentPath: string, hoverData: HoverInfo, view: EditorView) => true | undefined;
|
|
55
|
+
};
|
|
56
|
+
export function tsGoto(opts: GoToOptions = {}) {
|
|
57
|
+
return EditorView.domEventHandlers({
|
|
58
|
+
mousedown: (event, view) => {
|
|
59
|
+
const config = view.state.facet(tsFacet);
|
|
60
|
+
if (!config?.worker || !opts.gotoHandler) return false;
|
|
61
|
+
|
|
62
|
+
if (!(useMeta ? event.metaKey : event.ctrlKey)) return false;
|
|
63
|
+
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
|
|
66
|
+
const pos = view.posAtCoords({
|
|
67
|
+
x: event.clientX,
|
|
68
|
+
y: event.clientY,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (pos === null) return;
|
|
72
|
+
|
|
73
|
+
config.worker
|
|
74
|
+
.getHover({
|
|
75
|
+
path: config.path,
|
|
76
|
+
pos,
|
|
77
|
+
})
|
|
78
|
+
.then((hoverData: HoverInfo | null) => {
|
|
79
|
+
if (hoverData && opts.gotoHandler) {
|
|
80
|
+
opts.gotoHandler(config.path, hoverData, view);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return true;
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function createTypescript(signal: AbortSignal, doc: string): Promise<Language> {
|
|
90
|
+
const { SandboxWorker } = await import('../sandbox-worker/index.js');
|
|
91
|
+
const innerWorker: Worker = new SandboxWorker(tsWorkerURL, { name: 'typescript' });
|
|
92
|
+
signal.addEventListener('abort', () => innerWorker.terminate());
|
|
93
|
+
const worker = Comlink.wrap<API>(innerWorker);
|
|
94
|
+
await worker.stlInit({
|
|
95
|
+
typescriptTypes: typescriptTypes!,
|
|
96
|
+
suggestedEnv: authData!
|
|
97
|
+
.flatMap((e) => e.opts)
|
|
98
|
+
.map((e) => e.read_env)
|
|
99
|
+
.filter((_) => typeof _ === 'string'),
|
|
100
|
+
});
|
|
101
|
+
await worker.initialize();
|
|
102
|
+
function renderTooltip({ quickInfo }: HoverInfo) {
|
|
103
|
+
const info = quickInfo as QuickInfo;
|
|
104
|
+
const elt = document.createElement('div');
|
|
105
|
+
elt.className = 'cm-lsp-hover-tooltip cm-lsp-documentation';
|
|
106
|
+
const documentation = displayPartsToString(info.documentation);
|
|
107
|
+
const tags = info.tags ? info.tags.map((tag) => tagToString(tag)).join(' \n\n') : '';
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
dom: Object.assign(document.createElement('div'), {
|
|
111
|
+
innerHTML: markdown(
|
|
112
|
+
displayPartsToString(info.displayParts)
|
|
113
|
+
.split('\n')
|
|
114
|
+
.map((e) => '```typescript\n' + e + '\n```\n')
|
|
115
|
+
.join('') +
|
|
116
|
+
(documentation ? '\n\n---\n' + documentation : '') +
|
|
117
|
+
(tags ? '\n\n---\n' + tags : ''),
|
|
118
|
+
),
|
|
119
|
+
}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
let runnerWorker: Worker;
|
|
123
|
+
let runnerAPI: Remote<RunnerAPI>;
|
|
124
|
+
const startRunner = () => {
|
|
125
|
+
if (runnerWorker) {
|
|
126
|
+
runnerWorker.terminate();
|
|
127
|
+
runnerAPI[Comlink.releaseProxy]();
|
|
128
|
+
}
|
|
129
|
+
runnerWorker = new SandboxWorker(runnerWorkerURL);
|
|
130
|
+
runnerAPI = Comlink.wrap<RunnerAPI>(runnerWorker);
|
|
131
|
+
runnerAPI.setLogger(
|
|
132
|
+
proxy((...args) => {
|
|
133
|
+
logger(...args);
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
startRunner();
|
|
138
|
+
|
|
139
|
+
let logger: Logger;
|
|
140
|
+
return {
|
|
141
|
+
transport: undefined,
|
|
142
|
+
fileName: 'playground.mts',
|
|
143
|
+
extensions: [
|
|
144
|
+
typescriptLanguage,
|
|
145
|
+
tsFacet.of({ worker, path: '/play/playground.mts' }),
|
|
146
|
+
tsSync(),
|
|
147
|
+
tsLinter(),
|
|
148
|
+
autocompletion({
|
|
149
|
+
override: [tsAutocomplete()],
|
|
150
|
+
}),
|
|
151
|
+
tsHover({
|
|
152
|
+
renderTooltip,
|
|
153
|
+
}),
|
|
154
|
+
tsGoto({
|
|
155
|
+
gotoHandler(_path, hoverData, view) {
|
|
156
|
+
const def = hoverData.def?.[0] ?? hoverData.typeDef?.[0];
|
|
157
|
+
if (def?.originalFileName === '/play/playground.mts') {
|
|
158
|
+
view.dispatch({ selection: { anchor: def.textSpan.start, head: def.textSpan.start } });
|
|
159
|
+
}
|
|
160
|
+
console.log(def?.originalFileName);
|
|
161
|
+
return undefined;
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
tsTwoslash(),
|
|
165
|
+
],
|
|
166
|
+
doc,
|
|
167
|
+
setLogger(fn) {
|
|
168
|
+
logger = fn;
|
|
169
|
+
},
|
|
170
|
+
async expandLogHandle(handle) {
|
|
171
|
+
return await runnerAPI.expandLogHandle(handle);
|
|
172
|
+
},
|
|
173
|
+
async freeLogHandle(handle) {
|
|
174
|
+
return await runnerAPI.freeLogHandle(handle);
|
|
175
|
+
},
|
|
176
|
+
async setEnv(vars) {
|
|
177
|
+
return await runnerAPI.setEnv(vars);
|
|
178
|
+
},
|
|
179
|
+
async run(code, signal) {
|
|
180
|
+
signal.addEventListener('abort', startRunner);
|
|
181
|
+
try {
|
|
182
|
+
const transpiled = await worker.transform(code);
|
|
183
|
+
await runnerAPI.run(transpiled);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
if (signal.aborted) {
|
|
186
|
+
logger({ type: 'error', parts: ['Aborted.'] });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
throw e;
|
|
190
|
+
} finally {
|
|
191
|
+
signal.removeEventListener('abort', startRunner);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
async format(code) {
|
|
195
|
+
return worker.format(code);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
package/src/create.tsx
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { signal } from '@preact/signals-core';
|
|
4
|
+
import { Editor } from './codemirror/react';
|
|
5
|
+
import { LogsContext } from './logs-context';
|
|
6
|
+
import './playground.css';
|
|
7
|
+
|
|
8
|
+
export type PlaygroundLanguage = 'python' | 'typescript' | 'http';
|
|
9
|
+
|
|
10
|
+
export function createPlayground(props: {
|
|
11
|
+
lang: PlaygroundLanguage;
|
|
12
|
+
doc: string;
|
|
13
|
+
/** div.stl-snippet-request-container */
|
|
14
|
+
container: HTMLElement;
|
|
15
|
+
}): () => Promise<void> {
|
|
16
|
+
const ready = new Promise<{ element: HTMLDivElement; onShow(): void }>((resolve) => {
|
|
17
|
+
const element = document.createElement('div');
|
|
18
|
+
const root = createRoot(element);
|
|
19
|
+
const nodes = (
|
|
20
|
+
<StrictMode>
|
|
21
|
+
<LogsContext value={signal([])}>
|
|
22
|
+
<Editor
|
|
23
|
+
{...props}
|
|
24
|
+
unmount={() => {
|
|
25
|
+
props.container.style.display = '';
|
|
26
|
+
root.unmount();
|
|
27
|
+
element.remove();
|
|
28
|
+
}}
|
|
29
|
+
onLoad={(onShow) => {
|
|
30
|
+
resolve({ element, onShow });
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
</LogsContext>
|
|
34
|
+
</StrictMode>
|
|
35
|
+
);
|
|
36
|
+
root.render(nodes);
|
|
37
|
+
});
|
|
38
|
+
return () =>
|
|
39
|
+
ready.then(({ element, onShow }) => {
|
|
40
|
+
props.container.insertAdjacentElement('afterend', element);
|
|
41
|
+
props.container.style.display = 'none';
|
|
42
|
+
onShow();
|
|
43
|
+
});
|
|
44
|
+
}
|
package/src/icon.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import style from '@stainless-api/docs-ui/style';
|
|
2
|
+
|
|
3
|
+
export function PlaygroundIcon() {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
+
width={16}
|
|
8
|
+
height={16}
|
|
9
|
+
viewBox="0 0 24 24"
|
|
10
|
+
fill="none"
|
|
11
|
+
stroke="currentColor"
|
|
12
|
+
strokeWidth={2}
|
|
13
|
+
strokeLinecap="round"
|
|
14
|
+
strokeLinejoin="round"
|
|
15
|
+
className={'lucide ' + style.Icon}
|
|
16
|
+
aria-hidden="true"
|
|
17
|
+
>
|
|
18
|
+
<path d="m 1,2 h 1 a 4,4 0 0 1 4,4 v 1 m 5,15 H 10 A 4,4 0 0 1 6,18 V 6 a 4,4 0 0 1 4,-4 h 1 M 1,22 H 2 A 4,4 0 0 0 6,18 V 17 M 14.029059,8.147837 A 1.2853426,1.2853426 0 0 1 15.978924,7.0437277 L 22.40178,10.8959 a 1.2853426,1.2853426 0 0 1 0,2.208219 l -6.422856,3.852172 a 1.2853426,1.2853426 0 0 1 -1.949865,-1.105395 z" />
|
|
19
|
+
</svg>
|
|
20
|
+
);
|
|
21
|
+
}
|
package/src/index.ts
ADDED