browser-playground-core 0.0.1
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/dist/index.d.mts +114 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.js +1252 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1206 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +40 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1206 @@
|
|
|
1
|
+
// src/runtimePolyfills.ts
|
|
2
|
+
var ensureBrowserCompat = () => {
|
|
3
|
+
const g = globalThis;
|
|
4
|
+
if (!g.process) {
|
|
5
|
+
g.process = { env: {} };
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (!g.process.env) {
|
|
9
|
+
g.process.env = {};
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
ensureBrowserCompat();
|
|
13
|
+
|
|
14
|
+
// src/Playground.tsx
|
|
15
|
+
import { useMemo as useMemo3 } from "react";
|
|
16
|
+
|
|
17
|
+
// src/PlaygroundEditor.tsx
|
|
18
|
+
import { useCallback, useEffect as useEffect2, useRef, useState as useState2 } from "react";
|
|
19
|
+
import Editor from "@monaco-editor/react";
|
|
20
|
+
|
|
21
|
+
// src/PlaygroundProvider.tsx
|
|
22
|
+
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
23
|
+
|
|
24
|
+
// src/compiler.ts
|
|
25
|
+
import * as Babel from "@babel/standalone";
|
|
26
|
+
import { rollup } from "@rollup/browser";
|
|
27
|
+
|
|
28
|
+
// src/dependencies.ts
|
|
29
|
+
var depIdentifierForPackage = (pkgName) => {
|
|
30
|
+
const cleaned = pkgName.replace(/^@/, "").replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
31
|
+
return `__pg_dep_${cleaned}`;
|
|
32
|
+
};
|
|
33
|
+
var extendCompileConfigWithDependencies = (config, pkgNames) => {
|
|
34
|
+
if (!pkgNames.length) return config;
|
|
35
|
+
const allowedBareImports = new Set(config.allowedBareImports);
|
|
36
|
+
const rollupExternal = new Set(config.rollupExternal);
|
|
37
|
+
const rollupGlobals = { ...config.rollupGlobals };
|
|
38
|
+
for (const pkgName of pkgNames) {
|
|
39
|
+
allowedBareImports.add(pkgName);
|
|
40
|
+
rollupExternal.add(pkgName);
|
|
41
|
+
if (rollupGlobals[pkgName] == null) {
|
|
42
|
+
rollupGlobals[pkgName] = depIdentifierForPackage(pkgName);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
...config,
|
|
47
|
+
allowedBareImports: Array.from(allowedBareImports),
|
|
48
|
+
rollupExternal: Array.from(rollupExternal),
|
|
49
|
+
rollupGlobals
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/compiler.ts
|
|
54
|
+
var defaultBabelPlugins = [];
|
|
55
|
+
var babelTransform = (code, filename = "UserComponent.tsx") => {
|
|
56
|
+
var _a;
|
|
57
|
+
const result = Babel.transform(code, {
|
|
58
|
+
filename,
|
|
59
|
+
presets: [
|
|
60
|
+
["env", { modules: false, targets: { esmodules: true } }],
|
|
61
|
+
"typescript",
|
|
62
|
+
["react", { runtime: "classic" }]
|
|
63
|
+
],
|
|
64
|
+
parserOpts: {
|
|
65
|
+
plugins: defaultBabelPlugins
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return (_a = result.code) != null ? _a : "";
|
|
69
|
+
};
|
|
70
|
+
async function compileUserCode(rawCode, plugins = []) {
|
|
71
|
+
return compileVirtualFiles({ "/UserPreview.tsx": rawCode }, "/UserPreview.tsx", plugins);
|
|
72
|
+
}
|
|
73
|
+
async function compileVirtualFiles(rawFiles, entryFile, plugins = [], options) {
|
|
74
|
+
var _a;
|
|
75
|
+
let files = normalizeVirtualFileSystem(rawFiles);
|
|
76
|
+
let entry = normalizeVfsPath(entryFile);
|
|
77
|
+
if (!files[entry]) {
|
|
78
|
+
const first = Object.keys(files)[0];
|
|
79
|
+
return { code: null, error: `Entry file not found: "${entry}". Available files: ${first ? `"${first}"...` : "(none)"}` };
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
for (const plugin of plugins) {
|
|
83
|
+
if (!plugin.transformVirtualFiles) continue;
|
|
84
|
+
const transformed = await plugin.transformVirtualFiles({ files, entryFile: entry });
|
|
85
|
+
if (!transformed) continue;
|
|
86
|
+
files = normalizeVirtualFileSystem(transformed.files);
|
|
87
|
+
entry = normalizeVfsPath(transformed.entryFile);
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return { code: null, error: formatError(error) };
|
|
91
|
+
}
|
|
92
|
+
if (!files[entry]) {
|
|
93
|
+
return { code: null, error: `Entry file not found after plugin transforms: "${entry}".` };
|
|
94
|
+
}
|
|
95
|
+
const compileConfigBase = resolveCompileConfig(plugins, { files, entryFile: entry });
|
|
96
|
+
const compileConfig = extendCompileConfigWithDependencies(compileConfigBase, (_a = options == null ? void 0 : options.dependencies) != null ? _a : []);
|
|
97
|
+
let workingFiles = { ...files };
|
|
98
|
+
try {
|
|
99
|
+
for (const plugin of plugins) {
|
|
100
|
+
if (!plugin.beforeCompile) continue;
|
|
101
|
+
const next = {};
|
|
102
|
+
for (const [path, code] of Object.entries(workingFiles)) {
|
|
103
|
+
next[path] = await plugin.beforeCompile(code);
|
|
104
|
+
}
|
|
105
|
+
workingFiles = next;
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return { code: null, error: formatError(error) };
|
|
109
|
+
}
|
|
110
|
+
let bundled;
|
|
111
|
+
try {
|
|
112
|
+
bundled = await bundleVirtualFilesWithRollup(workingFiles, entry, compileConfig);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return { code: null, error: formatError(error) };
|
|
115
|
+
}
|
|
116
|
+
const wrapped = wrapIifeExecutable(bundled, compileConfig.iifeName);
|
|
117
|
+
let result = {
|
|
118
|
+
code: wrapped,
|
|
119
|
+
error: null,
|
|
120
|
+
runtime: compileConfig.runtime,
|
|
121
|
+
runtimeGlobalNames: Array.from(new Set(Object.values(compileConfig.rollupGlobals)))
|
|
122
|
+
};
|
|
123
|
+
try {
|
|
124
|
+
for (const plugin of plugins) {
|
|
125
|
+
if (plugin.afterCompile) {
|
|
126
|
+
result = await plugin.afterCompile(result);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return { code: null, error: formatError(error) };
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
async function bundleWithRollup(source) {
|
|
135
|
+
const code = await bundleVirtualFilesWithRollup({ "/entry.tsx": source }, "/entry.tsx", defaultReactCompileConfig());
|
|
136
|
+
return code;
|
|
137
|
+
}
|
|
138
|
+
var wrapIifeExecutable = (bundledCode, globalName) => {
|
|
139
|
+
return `${bundledCode}
|
|
140
|
+
const __playground_export__ =
|
|
141
|
+
typeof ${globalName} !== 'undefined' && ${globalName} && (${globalName}.default ?? ${globalName});
|
|
142
|
+
return __playground_export__;`;
|
|
143
|
+
};
|
|
144
|
+
var bundleVirtualFilesWithRollup = async (files, entryFile, config) => {
|
|
145
|
+
var _a, _b;
|
|
146
|
+
const bundle = await rollup({
|
|
147
|
+
input: entryFile,
|
|
148
|
+
external: config.rollupExternal,
|
|
149
|
+
plugins: [
|
|
150
|
+
{
|
|
151
|
+
name: "virtual-fs",
|
|
152
|
+
resolveId(source, importer) {
|
|
153
|
+
if (config.allowedBareImports.includes(source)) return null;
|
|
154
|
+
const importerPath = importer ? stripQuery(importer) : null;
|
|
155
|
+
if (!importerPath && source === entryFile) {
|
|
156
|
+
return source;
|
|
157
|
+
}
|
|
158
|
+
if (source.startsWith("/") || source.startsWith("./") || source.startsWith("../")) {
|
|
159
|
+
const resolved = resolveVfsImport(source, importerPath != null ? importerPath : entryFile, files);
|
|
160
|
+
if (!resolved) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Cannot resolve import "${source}" from "${importerPath != null ? importerPath : "(entry)"}".`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return resolved;
|
|
166
|
+
}
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Only relative/absolute imports are supported in this sandbox; tried to import "${source}".`
|
|
169
|
+
);
|
|
170
|
+
},
|
|
171
|
+
load(id) {
|
|
172
|
+
const path = stripQuery(id);
|
|
173
|
+
if (files[path] == null) return null;
|
|
174
|
+
return files[path];
|
|
175
|
+
},
|
|
176
|
+
transform(code, id) {
|
|
177
|
+
const path = stripQuery(id);
|
|
178
|
+
if (files[path] == null) return null;
|
|
179
|
+
const compiled = babelTransform(code, path);
|
|
180
|
+
return { code: compiled, map: null };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
});
|
|
185
|
+
const { output } = await bundle.generate({
|
|
186
|
+
format: "iife",
|
|
187
|
+
name: config.iifeName,
|
|
188
|
+
globals: config.rollupGlobals
|
|
189
|
+
});
|
|
190
|
+
await bundle.close();
|
|
191
|
+
return (_b = (_a = output[0]) == null ? void 0 : _a.code) != null ? _b : "";
|
|
192
|
+
};
|
|
193
|
+
var stripQuery = (id) => {
|
|
194
|
+
var _a;
|
|
195
|
+
return (_a = id.split("?")[0]) != null ? _a : id;
|
|
196
|
+
};
|
|
197
|
+
var normalizeVirtualFileSystem = (files) => {
|
|
198
|
+
const normalized = {};
|
|
199
|
+
for (const [path, code] of Object.entries(files)) {
|
|
200
|
+
normalized[normalizeVfsPath(path)] = code;
|
|
201
|
+
}
|
|
202
|
+
return normalized;
|
|
203
|
+
};
|
|
204
|
+
var normalizeVfsPath = (path) => {
|
|
205
|
+
const replaced = path.replace(/\\/g, "/");
|
|
206
|
+
const ensured = replaced.startsWith("/") ? replaced : `/${replaced}`;
|
|
207
|
+
return ensured.replace(/\/+/g, "/");
|
|
208
|
+
};
|
|
209
|
+
var resolveVfsImport = (source, importer, files) => {
|
|
210
|
+
const baseDir = dirname(importer);
|
|
211
|
+
const baseResolved = source.startsWith("/") ? normalizeVfsPath(source) : normalizeVfsPath(resolvePath(baseDir, source));
|
|
212
|
+
const candidates = fileCandidates(baseResolved);
|
|
213
|
+
for (const candidate of candidates) {
|
|
214
|
+
if (files[candidate] != null) return candidate;
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
};
|
|
218
|
+
var fileCandidates = (path) => {
|
|
219
|
+
const hasExt = /\.[a-zA-Z0-9]+$/.test(path);
|
|
220
|
+
const exts = [".ts", ".tsx", ".js", ".jsx", ".vue"];
|
|
221
|
+
const candidates = [];
|
|
222
|
+
if (hasExt) {
|
|
223
|
+
candidates.push(path);
|
|
224
|
+
return candidates;
|
|
225
|
+
}
|
|
226
|
+
for (const ext of exts) candidates.push(`${path}${ext}`);
|
|
227
|
+
for (const ext of exts) candidates.push(`${path}/index${ext}`);
|
|
228
|
+
return candidates;
|
|
229
|
+
};
|
|
230
|
+
var dirname = (path) => {
|
|
231
|
+
const normalized = normalizeVfsPath(path);
|
|
232
|
+
const idx = normalized.lastIndexOf("/");
|
|
233
|
+
if (idx <= 0) return "/";
|
|
234
|
+
return normalized.slice(0, idx);
|
|
235
|
+
};
|
|
236
|
+
var resolvePath = (baseDir, relative) => {
|
|
237
|
+
const baseParts = normalizeVfsPath(baseDir).split("/").filter(Boolean);
|
|
238
|
+
const relParts = relative.split("/").filter(Boolean);
|
|
239
|
+
const parts = [...baseParts];
|
|
240
|
+
for (const part of relParts) {
|
|
241
|
+
if (part === ".") continue;
|
|
242
|
+
if (part === "..") {
|
|
243
|
+
parts.pop();
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
parts.push(part);
|
|
247
|
+
}
|
|
248
|
+
return `/${parts.join("/")}`;
|
|
249
|
+
};
|
|
250
|
+
var formatError = (error) => {
|
|
251
|
+
if (error instanceof Error) return error.message;
|
|
252
|
+
return typeof error === "string" ? error : "Unknown error";
|
|
253
|
+
};
|
|
254
|
+
var defaultReactCompileConfig = () => ({
|
|
255
|
+
runtime: "react",
|
|
256
|
+
allowedBareImports: ["react"],
|
|
257
|
+
rollupExternal: ["react"],
|
|
258
|
+
rollupGlobals: { react: "React" },
|
|
259
|
+
iifeName: "__PlaygroundPreview__"
|
|
260
|
+
});
|
|
261
|
+
var resolveCompileConfig = (plugins, ctx) => {
|
|
262
|
+
var _a;
|
|
263
|
+
let config = defaultReactCompileConfig();
|
|
264
|
+
for (const plugin of plugins) {
|
|
265
|
+
if (!plugin.extendCompileConfig) continue;
|
|
266
|
+
config = (_a = plugin.extendCompileConfig(config, ctx)) != null ? _a : config;
|
|
267
|
+
}
|
|
268
|
+
return config;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/mapping.ts
|
|
272
|
+
import { parse } from "@babel/parser";
|
|
273
|
+
import traverse from "@babel/traverse";
|
|
274
|
+
var PARSER_PLUGINS = ["typescript", "jsx"];
|
|
275
|
+
var MAPPING_TAG = "@pg-mapping";
|
|
276
|
+
function applyFormValueToFiles(files, formValue) {
|
|
277
|
+
const next = {};
|
|
278
|
+
for (const [filePath, code] of Object.entries(files)) {
|
|
279
|
+
next[filePath] = applyFormValueToCode(code, formValue);
|
|
280
|
+
}
|
|
281
|
+
return next;
|
|
282
|
+
}
|
|
283
|
+
function applyFormValueToCode(code, formValue) {
|
|
284
|
+
const mappings = extractMappingsFromCode(code, "/__inline__");
|
|
285
|
+
if (mappings.length === 0) return code;
|
|
286
|
+
const replacements = [];
|
|
287
|
+
for (const mapping of mappings) {
|
|
288
|
+
const value = getByPath(formValue, mapping.path);
|
|
289
|
+
if (value === void 0) continue;
|
|
290
|
+
const text = literalToSource(value, mapping.quoteHint);
|
|
291
|
+
if (text == null) continue;
|
|
292
|
+
replacements.push({ start: mapping.initRange.start, end: mapping.initRange.end, text });
|
|
293
|
+
}
|
|
294
|
+
if (replacements.length === 0) return code;
|
|
295
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
296
|
+
let out = code;
|
|
297
|
+
for (const r of replacements) {
|
|
298
|
+
out = out.slice(0, r.start) + r.text + out.slice(r.end);
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
function extractMappedFormValue(files) {
|
|
303
|
+
const out = {};
|
|
304
|
+
for (const [filePath, code] of Object.entries(files)) {
|
|
305
|
+
const mappings = extractMappingsFromCode(code, filePath);
|
|
306
|
+
for (const mapping of mappings) {
|
|
307
|
+
if (mapping.initValue === void 0) continue;
|
|
308
|
+
setByPath(out, mapping.path, mapping.initValue);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
function extractMappingsFromCode(code, filePath) {
|
|
314
|
+
let ast;
|
|
315
|
+
try {
|
|
316
|
+
ast = parse(code, { sourceType: "module", plugins: PARSER_PLUGINS, ranges: true, tokens: true });
|
|
317
|
+
} catch {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
const mappings = [];
|
|
321
|
+
try {
|
|
322
|
+
traverse(ast, {
|
|
323
|
+
VariableDeclarator(path) {
|
|
324
|
+
var _a, _b;
|
|
325
|
+
const declNode = path.node;
|
|
326
|
+
const parentNode = (_a = path.parentPath) == null ? void 0 : _a.node;
|
|
327
|
+
const comment = (_b = findMappingComment(declNode.leadingComments)) != null ? _b : findMappingComment(parentNode == null ? void 0 : parentNode.leadingComments);
|
|
328
|
+
if (!comment) return;
|
|
329
|
+
const mappingPath = parseMappingPath(comment);
|
|
330
|
+
if (!mappingPath) return;
|
|
331
|
+
const init = declNode.init;
|
|
332
|
+
if (!init || typeof init.start !== "number" || typeof init.end !== "number") return;
|
|
333
|
+
const { value, quoteHint } = extractLiteralValueWithHint(code, init);
|
|
334
|
+
if (value === void 0) return;
|
|
335
|
+
mappings.push({
|
|
336
|
+
filePath,
|
|
337
|
+
path: mappingPath,
|
|
338
|
+
initRange: { start: init.start, end: init.end },
|
|
339
|
+
initValue: value,
|
|
340
|
+
quoteHint
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
} catch {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
return mappings;
|
|
348
|
+
}
|
|
349
|
+
var findMappingComment = (comments) => {
|
|
350
|
+
if (!comments || comments.length === 0) return null;
|
|
351
|
+
for (const c of comments) {
|
|
352
|
+
const value = c == null ? void 0 : c.value;
|
|
353
|
+
if (!value) continue;
|
|
354
|
+
if (value.includes(MAPPING_TAG)) return value;
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
};
|
|
358
|
+
var parseMappingPath = (commentValue) => {
|
|
359
|
+
const idx = commentValue.indexOf(MAPPING_TAG);
|
|
360
|
+
if (idx < 0) return null;
|
|
361
|
+
const after = commentValue.slice(idx + MAPPING_TAG.length).trim();
|
|
362
|
+
const start = after.indexOf("[");
|
|
363
|
+
const end = after.lastIndexOf("]");
|
|
364
|
+
if (start < 0 || end < 0 || end <= start) return null;
|
|
365
|
+
const inside = after.slice(start, end + 1).trim();
|
|
366
|
+
try {
|
|
367
|
+
const jsonish = inside.replace(/'/g, '"');
|
|
368
|
+
const parsed = JSON.parse(jsonish);
|
|
369
|
+
if (!Array.isArray(parsed)) return null;
|
|
370
|
+
const path = [];
|
|
371
|
+
for (const seg of parsed) {
|
|
372
|
+
if (typeof seg === "string" || typeof seg === "number") path.push(seg);
|
|
373
|
+
else return null;
|
|
374
|
+
}
|
|
375
|
+
return path;
|
|
376
|
+
} catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
var extractLiteralValueWithHint = (code, initNode) => {
|
|
381
|
+
var _a, _b, _c, _d;
|
|
382
|
+
if ((initNode == null ? void 0 : initNode.type) === "StringLiteral") {
|
|
383
|
+
const hint = quoteHintFromSource(code.slice(initNode.start, initNode.end));
|
|
384
|
+
return { value: initNode.value, quoteHint: hint };
|
|
385
|
+
}
|
|
386
|
+
if ((initNode == null ? void 0 : initNode.type) === "NumericLiteral") return { value: initNode.value };
|
|
387
|
+
if ((initNode == null ? void 0 : initNode.type) === "BooleanLiteral") return { value: initNode.value };
|
|
388
|
+
if ((initNode == null ? void 0 : initNode.type) === "NullLiteral") return { value: null };
|
|
389
|
+
if ((initNode == null ? void 0 : initNode.type) === "TemplateLiteral" && Array.isArray(initNode.expressions) && initNode.expressions.length === 0) {
|
|
390
|
+
return { value: (_d = (_c = (_b = (_a = initNode.quasis) == null ? void 0 : _a[0]) == null ? void 0 : _b.value) == null ? void 0 : _c.cooked) != null ? _d : "", quoteHint: "`" };
|
|
391
|
+
}
|
|
392
|
+
return { value: void 0 };
|
|
393
|
+
};
|
|
394
|
+
var quoteHintFromSource = (raw) => {
|
|
395
|
+
const trimmed = raw.trim();
|
|
396
|
+
if (trimmed.startsWith("'")) return "'";
|
|
397
|
+
if (trimmed.startsWith('"')) return '"';
|
|
398
|
+
if (trimmed.startsWith("`")) return "`";
|
|
399
|
+
return void 0;
|
|
400
|
+
};
|
|
401
|
+
var literalToSource = (value, quoteHint) => {
|
|
402
|
+
if (value == null) return "null";
|
|
403
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : null;
|
|
404
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
405
|
+
if (typeof value !== "string") return null;
|
|
406
|
+
if (quoteHint === "`") {
|
|
407
|
+
const safe2 = value.replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
408
|
+
return `\`${safe2}\``;
|
|
409
|
+
}
|
|
410
|
+
if (quoteHint === "'") {
|
|
411
|
+
const safe2 = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
|
|
412
|
+
return `'${safe2}'`;
|
|
413
|
+
}
|
|
414
|
+
const safe = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
415
|
+
return `"${safe}"`;
|
|
416
|
+
};
|
|
417
|
+
var getByPath = (root, path) => {
|
|
418
|
+
let cur = root;
|
|
419
|
+
for (const seg of path) {
|
|
420
|
+
if (cur == null) return void 0;
|
|
421
|
+
cur = cur[seg];
|
|
422
|
+
}
|
|
423
|
+
return cur;
|
|
424
|
+
};
|
|
425
|
+
var setByPath = (root, path, value) => {
|
|
426
|
+
if (!root || path.length === 0) return;
|
|
427
|
+
let cur = root;
|
|
428
|
+
for (let i = 0; i < path.length; i++) {
|
|
429
|
+
const seg = path[i];
|
|
430
|
+
const isLast = i === path.length - 1;
|
|
431
|
+
if (isLast) {
|
|
432
|
+
cur[seg] = value;
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const nextSeg = path[i + 1];
|
|
436
|
+
const shouldBeArray = typeof nextSeg === "number";
|
|
437
|
+
if (cur[seg] == null || typeof cur[seg] !== "object") {
|
|
438
|
+
cur[seg] = shouldBeArray ? [] : {};
|
|
439
|
+
}
|
|
440
|
+
cur = cur[seg];
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// src/PlaygroundProvider.tsx
|
|
445
|
+
import { jsx } from "react/jsx-runtime";
|
|
446
|
+
var defaultSnippet = `import React from 'react';
|
|
447
|
+
|
|
448
|
+
type GreetingProps = { name: string };
|
|
449
|
+
|
|
450
|
+
const Card: React.FC<GreetingProps> = ({ name }) => {
|
|
451
|
+
return (
|
|
452
|
+
<div style={{ padding: '16px', borderRadius: '12px', background: '#0f172a', color: '#e2e8f0' }}>
|
|
453
|
+
<h2 style={{ margin: 0 }}>Hello, {name}</h2>
|
|
454
|
+
<p style={{ marginTop: 8 }}>You can edit this code to see live updates.</p>
|
|
455
|
+
</div>
|
|
456
|
+
);
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
export default function Preview() {
|
|
460
|
+
return (
|
|
461
|
+
<div style={{ display: 'grid', gap: '12px', padding: '8px' }}>
|
|
462
|
+
<Card name="Playground" />
|
|
463
|
+
<Card name="React" />
|
|
464
|
+
</div>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
`;
|
|
468
|
+
var PlaygroundContext = createContext(null);
|
|
469
|
+
var PlaygroundProvider = ({
|
|
470
|
+
initialCode = defaultSnippet,
|
|
471
|
+
initialFiles,
|
|
472
|
+
entryFile = "/App.tsx",
|
|
473
|
+
plugins = [],
|
|
474
|
+
dependencies,
|
|
475
|
+
formValue,
|
|
476
|
+
onFormValueChange,
|
|
477
|
+
children
|
|
478
|
+
}) => {
|
|
479
|
+
var _a;
|
|
480
|
+
const [files, setFiles] = useState(() => {
|
|
481
|
+
if (initialFiles && Object.keys(initialFiles).length > 0) return normalizeVirtualFileSystem2(initialFiles);
|
|
482
|
+
return { [normalizeVfsPath2(entryFile)]: initialCode };
|
|
483
|
+
});
|
|
484
|
+
const [activeFilePath, setActiveFilePath] = useState(() => {
|
|
485
|
+
var _a2;
|
|
486
|
+
const normalizedEntry = normalizeVfsPath2(entryFile);
|
|
487
|
+
const normalizedFiles = initialFiles ? normalizeVirtualFileSystem2(initialFiles) : null;
|
|
488
|
+
if (normalizedFiles && normalizedFiles[normalizedEntry] != null) return normalizedEntry;
|
|
489
|
+
return normalizedFiles ? (_a2 = Object.keys(normalizedFiles)[0]) != null ? _a2 : normalizedEntry : normalizedEntry;
|
|
490
|
+
});
|
|
491
|
+
const [error, setError] = useState(null);
|
|
492
|
+
const [isCompiling, setIsCompiling] = useState(false);
|
|
493
|
+
const [renderedComponent, setRenderedComponent] = useState(null);
|
|
494
|
+
const [renderedDomModule, setRenderedDomModule] = useState(null);
|
|
495
|
+
const [runtime, setRuntime] = useState("react");
|
|
496
|
+
const lastAppliedFormValueRef = React.useRef(void 0);
|
|
497
|
+
const lastEmittedFormValueRef = React.useRef(void 0);
|
|
498
|
+
useEffect(() => {
|
|
499
|
+
var _a2;
|
|
500
|
+
if (initialFiles && Object.keys(initialFiles).length > 0) {
|
|
501
|
+
const normalized = normalizeVirtualFileSystem2(initialFiles);
|
|
502
|
+
setFiles(normalized);
|
|
503
|
+
const normalizedEntry2 = normalizeVfsPath2(entryFile);
|
|
504
|
+
setActiveFilePath(normalized[normalizedEntry2] != null ? normalizedEntry2 : (_a2 = Object.keys(normalized)[0]) != null ? _a2 : normalizedEntry2);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const normalizedEntry = normalizeVfsPath2(entryFile);
|
|
508
|
+
setFiles({ [normalizedEntry]: initialCode });
|
|
509
|
+
setActiveFilePath(normalizedEntry);
|
|
510
|
+
}, [entryFile, initialCode, initialFiles]);
|
|
511
|
+
useEffect(() => {
|
|
512
|
+
let cancelled = false;
|
|
513
|
+
const timeout = window.setTimeout(() => {
|
|
514
|
+
setIsCompiling(true);
|
|
515
|
+
runCompilation(files, normalizeVfsPath2(entryFile), plugins, dependencies).then(async (result) => {
|
|
516
|
+
var _a2, _b, _c;
|
|
517
|
+
if (cancelled) return;
|
|
518
|
+
setIsCompiling(false);
|
|
519
|
+
if (result.error || !result.code) {
|
|
520
|
+
setError((_a2 = result.error) != null ? _a2 : "Unknown compilation error");
|
|
521
|
+
setRenderedComponent(null);
|
|
522
|
+
setRenderedDomModule(null);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const runtimeGlobalNames = (_b = result.runtimeGlobalNames) != null ? _b : ["React"];
|
|
527
|
+
const runtimeGlobals = await resolveRuntimeGlobals(runtimeGlobalNames, plugins, (_c = result.runtime) != null ? _c : "react", dependencies);
|
|
528
|
+
if (cancelled) return;
|
|
529
|
+
const createModule = new Function(...runtimeGlobalNames, result.code);
|
|
530
|
+
const moduleExport = createModule(...runtimeGlobals);
|
|
531
|
+
if (result.runtime === "dom") {
|
|
532
|
+
if (!moduleExport || typeof moduleExport.mount !== "function") {
|
|
533
|
+
throw new Error("DOM runtime must export an object with a mount(el) function as default export.");
|
|
534
|
+
}
|
|
535
|
+
setRuntime("dom");
|
|
536
|
+
setRenderedDomModule(() => moduleExport);
|
|
537
|
+
setRenderedComponent(null);
|
|
538
|
+
setError(null);
|
|
539
|
+
} else {
|
|
540
|
+
if (moduleExport) {
|
|
541
|
+
setRuntime("react");
|
|
542
|
+
setRenderedComponent(() => moduleExport);
|
|
543
|
+
setRenderedDomModule(null);
|
|
544
|
+
setError(null);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
} catch (err) {
|
|
548
|
+
setError(formatError2(err));
|
|
549
|
+
setRenderedComponent(null);
|
|
550
|
+
setRenderedDomModule(null);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}, 200);
|
|
554
|
+
return () => {
|
|
555
|
+
cancelled = true;
|
|
556
|
+
window.clearTimeout(timeout);
|
|
557
|
+
};
|
|
558
|
+
}, [dependencies, entryFile, files, plugins]);
|
|
559
|
+
useEffect(() => {
|
|
560
|
+
if (formValue === void 0) return;
|
|
561
|
+
if (isDeepEqual(formValue, lastAppliedFormValueRef.current)) return;
|
|
562
|
+
lastAppliedFormValueRef.current = formValue;
|
|
563
|
+
setFiles((prev) => {
|
|
564
|
+
const next = applyFormValueToFiles(prev, formValue);
|
|
565
|
+
return isDeepEqual(prev, next) ? prev : next;
|
|
566
|
+
});
|
|
567
|
+
}, [formValue]);
|
|
568
|
+
useEffect(() => {
|
|
569
|
+
if (!onFormValueChange) return;
|
|
570
|
+
let cancelled = false;
|
|
571
|
+
const timeout = window.setTimeout(() => {
|
|
572
|
+
if (cancelled) return;
|
|
573
|
+
const extracted = extractMappedFormValue(files);
|
|
574
|
+
const nextValue = mergeFormValue(formValue, extracted);
|
|
575
|
+
if (isDeepEqual(nextValue, formValue)) return;
|
|
576
|
+
if (isDeepEqual(nextValue, lastEmittedFormValueRef.current)) return;
|
|
577
|
+
lastEmittedFormValueRef.current = nextValue;
|
|
578
|
+
onFormValueChange(nextValue);
|
|
579
|
+
}, 200);
|
|
580
|
+
return () => {
|
|
581
|
+
cancelled = true;
|
|
582
|
+
window.clearTimeout(timeout);
|
|
583
|
+
};
|
|
584
|
+
}, [files, formValue, onFormValueChange]);
|
|
585
|
+
const updateFile = (path, code2) => {
|
|
586
|
+
const normalized = normalizeVfsPath2(path);
|
|
587
|
+
setFiles((prev) => ({ ...prev, [normalized]: code2 }));
|
|
588
|
+
};
|
|
589
|
+
const code = (_a = files[normalizeVfsPath2(activeFilePath)]) != null ? _a : "";
|
|
590
|
+
const setCode = (value2) => updateFile(activeFilePath, value2);
|
|
591
|
+
const value = useMemo(
|
|
592
|
+
() => ({
|
|
593
|
+
files,
|
|
594
|
+
activeFilePath,
|
|
595
|
+
setActiveFilePath: (path) => setActiveFilePath(normalizeVfsPath2(path)),
|
|
596
|
+
updateFile,
|
|
597
|
+
code,
|
|
598
|
+
setCode,
|
|
599
|
+
isCompiling,
|
|
600
|
+
error,
|
|
601
|
+
runtime,
|
|
602
|
+
renderedComponent,
|
|
603
|
+
renderedDomModule,
|
|
604
|
+
plugins
|
|
605
|
+
}),
|
|
606
|
+
[activeFilePath, code, error, files, isCompiling, plugins, renderedComponent, renderedDomModule, runtime]
|
|
607
|
+
);
|
|
608
|
+
return /* @__PURE__ */ jsx(PlaygroundContext.Provider, { value, children });
|
|
609
|
+
};
|
|
610
|
+
var usePlayground = () => {
|
|
611
|
+
const ctx = useContext(PlaygroundContext);
|
|
612
|
+
if (!ctx) {
|
|
613
|
+
throw new Error("usePlayground must be used within a PlaygroundProvider");
|
|
614
|
+
}
|
|
615
|
+
return ctx;
|
|
616
|
+
};
|
|
617
|
+
var runCompilation = async (files, entryFile, plugins, dependencies) => {
|
|
618
|
+
const result = await compileVirtualFiles(files, entryFile, plugins, { dependencies: Object.keys(dependencies != null ? dependencies : {}) });
|
|
619
|
+
return result;
|
|
620
|
+
};
|
|
621
|
+
var formatError2 = (error) => {
|
|
622
|
+
if (error instanceof Error) return error.message;
|
|
623
|
+
return typeof error === "string" ? error : "Unknown runtime error";
|
|
624
|
+
};
|
|
625
|
+
var normalizeVirtualFileSystem2 = (rawFiles) => {
|
|
626
|
+
const normalized = {};
|
|
627
|
+
for (const [path, code] of Object.entries(rawFiles)) {
|
|
628
|
+
normalized[normalizeVfsPath2(path)] = code;
|
|
629
|
+
}
|
|
630
|
+
return normalized;
|
|
631
|
+
};
|
|
632
|
+
var normalizeVfsPath2 = (path) => {
|
|
633
|
+
const replaced = path.replace(/\\/g, "/");
|
|
634
|
+
const ensured = replaced.startsWith("/") ? replaced : `/${replaced}`;
|
|
635
|
+
return ensured.replace(/\/+/g, "/");
|
|
636
|
+
};
|
|
637
|
+
var resolveRuntimeGlobals = async (names, plugins, runtime, dependencies) => {
|
|
638
|
+
const globals = { React };
|
|
639
|
+
const depIdToPkg = {};
|
|
640
|
+
for (const plugin of plugins) {
|
|
641
|
+
if (!plugin.runtimeGlobals) continue;
|
|
642
|
+
const provided = await plugin.runtimeGlobals({ runtime });
|
|
643
|
+
Object.assign(globals, provided != null ? provided : {});
|
|
644
|
+
}
|
|
645
|
+
for (const [pkgName, value] of Object.entries(dependencies != null ? dependencies : {})) {
|
|
646
|
+
const id = depIdentifierForPackage(pkgName);
|
|
647
|
+
depIdToPkg[id] = pkgName;
|
|
648
|
+
globals[id] = value;
|
|
649
|
+
}
|
|
650
|
+
return names.map((n) => {
|
|
651
|
+
if (globals[n] === void 0) {
|
|
652
|
+
const pkgName = depIdToPkg[n];
|
|
653
|
+
throw new Error(
|
|
654
|
+
pkgName ? `Missing dependency "${pkgName}" (runtime global "${n}"). Pass it via Playground props \`dependencies\` (e.g. \`dependencies={{ "${pkgName}": yourImportedModule }}\`).` : `Missing runtime global "${n}". If you imported a third-party package, pass it via Playground props \`dependencies\`, or provide it via a plugin \`runtimeGlobals\`.`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
return globals[n];
|
|
658
|
+
});
|
|
659
|
+
};
|
|
660
|
+
var mergeFormValue = (base, overlay) => {
|
|
661
|
+
if (!isPlainObject(base) || !isPlainObject(overlay)) return overlay;
|
|
662
|
+
return deepMergeObjects(base, overlay);
|
|
663
|
+
};
|
|
664
|
+
var deepMergeObjects = (base, overlay) => {
|
|
665
|
+
const out = { ...base };
|
|
666
|
+
for (const [k, v] of Object.entries(overlay)) {
|
|
667
|
+
const baseValue = out[k];
|
|
668
|
+
if (isPlainObject(baseValue) && isPlainObject(v)) {
|
|
669
|
+
out[k] = deepMergeObjects(baseValue, v);
|
|
670
|
+
} else {
|
|
671
|
+
out[k] = v;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return out;
|
|
675
|
+
};
|
|
676
|
+
var isPlainObject = (value) => {
|
|
677
|
+
if (value == null || typeof value !== "object") return false;
|
|
678
|
+
if (Array.isArray(value)) return false;
|
|
679
|
+
const proto = Object.getPrototypeOf(value);
|
|
680
|
+
return proto === Object.prototype || proto === null;
|
|
681
|
+
};
|
|
682
|
+
var isDeepEqual = (a, b) => {
|
|
683
|
+
if (a === b) return true;
|
|
684
|
+
if (a == null || b == null) return a === b;
|
|
685
|
+
if (typeof a !== typeof b) return false;
|
|
686
|
+
if (typeof a !== "object") return false;
|
|
687
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
688
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
689
|
+
if (a.length !== b.length) return false;
|
|
690
|
+
for (let i = 0; i < a.length; i++) {
|
|
691
|
+
if (!isDeepEqual(a[i], b[i])) return false;
|
|
692
|
+
}
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
const aKeys = Object.keys(a);
|
|
696
|
+
const bKeys = Object.keys(b);
|
|
697
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
698
|
+
for (const k of aKeys) {
|
|
699
|
+
if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
|
|
700
|
+
if (!isDeepEqual(a[k], b[k])) return false;
|
|
701
|
+
}
|
|
702
|
+
return true;
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
// src/PlaygroundFileTree.tsx
|
|
706
|
+
import { useMemo as useMemo2 } from "react";
|
|
707
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
708
|
+
var PlaygroundFileTree = ({
|
|
709
|
+
height = "100%",
|
|
710
|
+
width = 240,
|
|
711
|
+
className,
|
|
712
|
+
style
|
|
713
|
+
}) => {
|
|
714
|
+
const { files, activeFilePath, setActiveFilePath } = usePlayground();
|
|
715
|
+
const tree = useMemo2(() => buildTree(Object.keys(files)), [files]);
|
|
716
|
+
return /* @__PURE__ */ jsxs(
|
|
717
|
+
"div",
|
|
718
|
+
{
|
|
719
|
+
className,
|
|
720
|
+
style: {
|
|
721
|
+
border: "1px solid #e2e8f0",
|
|
722
|
+
borderRadius: 12,
|
|
723
|
+
overflow: "auto",
|
|
724
|
+
height,
|
|
725
|
+
width,
|
|
726
|
+
background: "#0b1224",
|
|
727
|
+
color: "#e2e8f0",
|
|
728
|
+
...style
|
|
729
|
+
},
|
|
730
|
+
children: [
|
|
731
|
+
/* @__PURE__ */ jsx2("div", { style: { padding: "10px 10px 6px", fontSize: 12, color: "#94a3b8", letterSpacing: 0.4 }, children: "EXPLORER" }),
|
|
732
|
+
/* @__PURE__ */ jsx2("div", { style: { padding: "0 6px 10px" }, children: tree.children.map((node) => /* @__PURE__ */ jsx2(
|
|
733
|
+
TreeNodeView,
|
|
734
|
+
{
|
|
735
|
+
node,
|
|
736
|
+
depth: 0,
|
|
737
|
+
activeFilePath,
|
|
738
|
+
onSelectFile: setActiveFilePath
|
|
739
|
+
},
|
|
740
|
+
node.path
|
|
741
|
+
)) })
|
|
742
|
+
]
|
|
743
|
+
}
|
|
744
|
+
);
|
|
745
|
+
};
|
|
746
|
+
var TreeNodeView = ({ node, depth, activeFilePath, onSelectFile }) => {
|
|
747
|
+
if (node.type === "folder") {
|
|
748
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
749
|
+
/* @__PURE__ */ jsx2(
|
|
750
|
+
"div",
|
|
751
|
+
{
|
|
752
|
+
style: {
|
|
753
|
+
padding: "4px 8px",
|
|
754
|
+
marginLeft: depth * 10,
|
|
755
|
+
fontSize: 12,
|
|
756
|
+
color: "#cbd5e1",
|
|
757
|
+
textTransform: "uppercase",
|
|
758
|
+
letterSpacing: 0.6
|
|
759
|
+
},
|
|
760
|
+
children: node.name
|
|
761
|
+
}
|
|
762
|
+
),
|
|
763
|
+
node.children.map((child) => /* @__PURE__ */ jsx2(
|
|
764
|
+
TreeNodeView,
|
|
765
|
+
{
|
|
766
|
+
node: child,
|
|
767
|
+
depth: depth + 1,
|
|
768
|
+
activeFilePath,
|
|
769
|
+
onSelectFile
|
|
770
|
+
},
|
|
771
|
+
child.path
|
|
772
|
+
))
|
|
773
|
+
] });
|
|
774
|
+
}
|
|
775
|
+
const isActive = normalizePath(node.path) === normalizePath(activeFilePath);
|
|
776
|
+
return /* @__PURE__ */ jsxs(
|
|
777
|
+
"button",
|
|
778
|
+
{
|
|
779
|
+
type: "button",
|
|
780
|
+
onClick: () => onSelectFile(node.path),
|
|
781
|
+
style: {
|
|
782
|
+
display: "flex",
|
|
783
|
+
alignItems: "center",
|
|
784
|
+
width: "100%",
|
|
785
|
+
textAlign: "left",
|
|
786
|
+
cursor: "pointer",
|
|
787
|
+
border: 0,
|
|
788
|
+
background: isActive ? "#1d2a4f" : "transparent",
|
|
789
|
+
color: isActive ? "#e2e8f0" : "#cbd5e1",
|
|
790
|
+
borderRadius: 8,
|
|
791
|
+
padding: "6px 8px",
|
|
792
|
+
marginLeft: depth * 10
|
|
793
|
+
},
|
|
794
|
+
children: [
|
|
795
|
+
/* @__PURE__ */ jsx2("span", { style: { width: 16, opacity: 0.8, marginRight: 6 }, children: "\u25CF" }),
|
|
796
|
+
/* @__PURE__ */ jsx2("span", { style: { fontSize: 13 }, children: node.name })
|
|
797
|
+
]
|
|
798
|
+
}
|
|
799
|
+
);
|
|
800
|
+
};
|
|
801
|
+
var buildTree = (paths) => {
|
|
802
|
+
const root = { type: "folder", name: "", path: "/", children: [] };
|
|
803
|
+
for (const rawPath of paths.map(normalizePath).sort()) {
|
|
804
|
+
const parts = rawPath.split("/").filter(Boolean);
|
|
805
|
+
let current = root;
|
|
806
|
+
let currentPath = "";
|
|
807
|
+
for (let i = 0; i < parts.length; i++) {
|
|
808
|
+
const part = parts[i];
|
|
809
|
+
currentPath += `/${part}`;
|
|
810
|
+
const isLeaf = i === parts.length - 1;
|
|
811
|
+
if (isLeaf) {
|
|
812
|
+
current.children.push({ type: "file", name: part, path: currentPath });
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
let folder = current.children.find(
|
|
816
|
+
(child) => child.type === "folder" && child.name === part
|
|
817
|
+
);
|
|
818
|
+
if (!folder) {
|
|
819
|
+
folder = { type: "folder", name: part, path: currentPath, children: [] };
|
|
820
|
+
current.children.push(folder);
|
|
821
|
+
}
|
|
822
|
+
current = folder;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
sortTree(root);
|
|
826
|
+
return root;
|
|
827
|
+
};
|
|
828
|
+
var sortTree = (node) => {
|
|
829
|
+
if (node.type !== "folder") return;
|
|
830
|
+
node.children.sort((a, b) => {
|
|
831
|
+
if (a.type !== b.type) return a.type === "folder" ? -1 : 1;
|
|
832
|
+
return a.name.localeCompare(b.name);
|
|
833
|
+
});
|
|
834
|
+
for (const child of node.children) sortTree(child);
|
|
835
|
+
};
|
|
836
|
+
var normalizePath = (path) => {
|
|
837
|
+
const replaced = path.replace(/\\/g, "/");
|
|
838
|
+
const ensured = replaced.startsWith("/") ? replaced : `/${replaced}`;
|
|
839
|
+
return ensured.replace(/\/+/g, "/");
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
// src/PlaygroundEditor.tsx
|
|
843
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
844
|
+
var PlaygroundEditor = ({
|
|
845
|
+
height = "100%",
|
|
846
|
+
className,
|
|
847
|
+
style,
|
|
848
|
+
language,
|
|
849
|
+
showFileTree,
|
|
850
|
+
fileTreeDefaultOpen = true,
|
|
851
|
+
fileTreeWidth = 240
|
|
852
|
+
}) => {
|
|
853
|
+
const { code, setCode, activeFilePath, files, plugins } = usePlayground();
|
|
854
|
+
const monacoRef = useRef(null);
|
|
855
|
+
const filesRef = useRef(files);
|
|
856
|
+
const pluginsRef = useRef(plugins);
|
|
857
|
+
const [isFileTreeOpen, setIsFileTreeOpen] = useState2(fileTreeDefaultOpen);
|
|
858
|
+
useEffect2(() => {
|
|
859
|
+
setIsFileTreeOpen(fileTreeDefaultOpen);
|
|
860
|
+
}, [fileTreeDefaultOpen]);
|
|
861
|
+
useEffect2(() => {
|
|
862
|
+
filesRef.current = files;
|
|
863
|
+
}, [files]);
|
|
864
|
+
useEffect2(() => {
|
|
865
|
+
pluginsRef.current = plugins;
|
|
866
|
+
}, [plugins]);
|
|
867
|
+
const handleBeforeMount = useCallback((monaco) => {
|
|
868
|
+
monacoRef.current = monaco;
|
|
869
|
+
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
|
870
|
+
// Required for TSX parsing/highlighting when models are backed by .tsx filenames.
|
|
871
|
+
jsx: monaco.languages.typescript.JsxEmit.Preserve,
|
|
872
|
+
target: monaco.languages.typescript.ScriptTarget.ESNext,
|
|
873
|
+
allowJs: true,
|
|
874
|
+
allowNonTsExtensions: true,
|
|
875
|
+
module: monaco.languages.typescript.ModuleKind.ESNext,
|
|
876
|
+
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
|
877
|
+
esModuleInterop: true,
|
|
878
|
+
allowSyntheticDefaultImports: true
|
|
879
|
+
});
|
|
880
|
+
monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true);
|
|
881
|
+
injectVirtualFilesIntoMonaco(monaco, filesRef.current);
|
|
882
|
+
for (const plugin of pluginsRef.current) {
|
|
883
|
+
if (!plugin.setupMonaco) continue;
|
|
884
|
+
try {
|
|
885
|
+
const result = plugin.setupMonaco(monaco);
|
|
886
|
+
if (result && typeof result.then === "function") {
|
|
887
|
+
result.catch((err) => console.warn("[playground] plugin.setupMonaco failed", err));
|
|
888
|
+
}
|
|
889
|
+
} catch (err) {
|
|
890
|
+
console.warn("[playground] plugin.setupMonaco failed", err);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}, []);
|
|
894
|
+
useEffect2(() => {
|
|
895
|
+
if (!monacoRef.current) return;
|
|
896
|
+
injectVirtualFilesIntoMonaco(monacoRef.current, files);
|
|
897
|
+
}, [files]);
|
|
898
|
+
return /* @__PURE__ */ jsxs2(
|
|
899
|
+
"div",
|
|
900
|
+
{
|
|
901
|
+
className,
|
|
902
|
+
style: {
|
|
903
|
+
border: "1px solid #e2e8f0",
|
|
904
|
+
borderRadius: 12,
|
|
905
|
+
overflow: "hidden",
|
|
906
|
+
height,
|
|
907
|
+
display: "flex",
|
|
908
|
+
minWidth: 0,
|
|
909
|
+
...style
|
|
910
|
+
},
|
|
911
|
+
children: [
|
|
912
|
+
shouldShowFileTree(showFileTree, files) && /* @__PURE__ */ jsx3(
|
|
913
|
+
"div",
|
|
914
|
+
{
|
|
915
|
+
style: {
|
|
916
|
+
width: isFileTreeOpen ? fileTreeWidth : 34,
|
|
917
|
+
borderRight: "1px solid #0f172a",
|
|
918
|
+
background: "#0b1224",
|
|
919
|
+
flex: "0 0 auto",
|
|
920
|
+
height: "100%"
|
|
921
|
+
},
|
|
922
|
+
children: isFileTreeOpen ? /* @__PURE__ */ jsxs2("div", { style: { height: "100%", position: "relative" }, children: [
|
|
923
|
+
/* @__PURE__ */ jsx3("div", { style: { position: "absolute", top: 8, right: 8, zIndex: 2 }, children: /* @__PURE__ */ jsx3(
|
|
924
|
+
"button",
|
|
925
|
+
{
|
|
926
|
+
type: "button",
|
|
927
|
+
onClick: () => {
|
|
928
|
+
setIsFileTreeOpen(false);
|
|
929
|
+
},
|
|
930
|
+
title: "Collapse",
|
|
931
|
+
style: iconButtonStyle,
|
|
932
|
+
children: "\u27E8"
|
|
933
|
+
}
|
|
934
|
+
) }),
|
|
935
|
+
/* @__PURE__ */ jsx3(
|
|
936
|
+
PlaygroundFileTree,
|
|
937
|
+
{
|
|
938
|
+
height: "100%",
|
|
939
|
+
width: "100%",
|
|
940
|
+
style: { border: "none", borderRadius: 0, background: "transparent" }
|
|
941
|
+
}
|
|
942
|
+
)
|
|
943
|
+
] }) : /* @__PURE__ */ jsx3(
|
|
944
|
+
"div",
|
|
945
|
+
{
|
|
946
|
+
style: {
|
|
947
|
+
height: "100%",
|
|
948
|
+
display: "flex",
|
|
949
|
+
alignItems: "center",
|
|
950
|
+
justifyContent: "center"
|
|
951
|
+
},
|
|
952
|
+
children: /* @__PURE__ */ jsx3(
|
|
953
|
+
"button",
|
|
954
|
+
{
|
|
955
|
+
type: "button",
|
|
956
|
+
onClick: () => {
|
|
957
|
+
setIsFileTreeOpen(true);
|
|
958
|
+
},
|
|
959
|
+
title: "Files",
|
|
960
|
+
style: iconButtonStyle,
|
|
961
|
+
children: "\u2261"
|
|
962
|
+
}
|
|
963
|
+
)
|
|
964
|
+
}
|
|
965
|
+
)
|
|
966
|
+
}
|
|
967
|
+
),
|
|
968
|
+
/* @__PURE__ */ jsx3("div", { style: { flex: 1, minWidth: 0, height: "100%" }, children: /* @__PURE__ */ jsx3(
|
|
969
|
+
Editor,
|
|
970
|
+
{
|
|
971
|
+
height: "100%",
|
|
972
|
+
defaultLanguage: language != null ? language : languageFromPath(activeFilePath),
|
|
973
|
+
path: `file://${activeFilePath}`,
|
|
974
|
+
theme: "vs-dark",
|
|
975
|
+
value: code,
|
|
976
|
+
onChange: (value) => setCode(value != null ? value : ""),
|
|
977
|
+
beforeMount: handleBeforeMount,
|
|
978
|
+
options: {
|
|
979
|
+
minimap: { enabled: false },
|
|
980
|
+
fontSize: 14,
|
|
981
|
+
smoothScrolling: true,
|
|
982
|
+
tabSize: 2,
|
|
983
|
+
fixedOverflowWidgets: true
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
) })
|
|
987
|
+
]
|
|
988
|
+
}
|
|
989
|
+
);
|
|
990
|
+
};
|
|
991
|
+
var shouldShowFileTree = (showFileTree, files) => {
|
|
992
|
+
if (typeof showFileTree === "boolean") return showFileTree;
|
|
993
|
+
return Object.keys(files).length > 1;
|
|
994
|
+
};
|
|
995
|
+
var iconButtonStyle = {
|
|
996
|
+
border: "1px solid rgba(226, 232, 240, 0.18)",
|
|
997
|
+
background: "rgba(15, 23, 42, 0.25)",
|
|
998
|
+
color: "#e2e8f0",
|
|
999
|
+
cursor: "pointer",
|
|
1000
|
+
borderRadius: 8,
|
|
1001
|
+
width: 26,
|
|
1002
|
+
height: 26,
|
|
1003
|
+
display: "inline-flex",
|
|
1004
|
+
alignItems: "center",
|
|
1005
|
+
justifyContent: "center",
|
|
1006
|
+
lineHeight: 1
|
|
1007
|
+
};
|
|
1008
|
+
var injectVirtualFilesIntoMonaco = (monaco, files) => {
|
|
1009
|
+
for (const [rawPath, code] of Object.entries(files)) {
|
|
1010
|
+
const normalizedPath = normalizeVfsPath3(rawPath);
|
|
1011
|
+
const uri = monaco.Uri.parse(`file://${normalizedPath}`);
|
|
1012
|
+
const existing = monaco.editor.getModel(uri);
|
|
1013
|
+
const language = languageFromPath(normalizedPath);
|
|
1014
|
+
if (!existing) {
|
|
1015
|
+
monaco.editor.createModel(code, language, uri);
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
if (existing.getLanguageId && existing.getLanguageId() !== language) {
|
|
1019
|
+
monaco.editor.setModelLanguage(existing, language);
|
|
1020
|
+
}
|
|
1021
|
+
if (existing.getValue() !== code) {
|
|
1022
|
+
existing.pushEditOperations(
|
|
1023
|
+
[],
|
|
1024
|
+
[
|
|
1025
|
+
{
|
|
1026
|
+
range: existing.getFullModelRange(),
|
|
1027
|
+
text: code
|
|
1028
|
+
}
|
|
1029
|
+
],
|
|
1030
|
+
() => null
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
var normalizeVfsPath3 = (path) => {
|
|
1036
|
+
const replaced = path.replace(/\\/g, "/");
|
|
1037
|
+
const ensured = replaced.startsWith("/") ? replaced : `/${replaced}`;
|
|
1038
|
+
return ensured.replace(/\/+/g, "/");
|
|
1039
|
+
};
|
|
1040
|
+
var languageFromPath = (path) => {
|
|
1041
|
+
const lower = path.toLowerCase();
|
|
1042
|
+
if (lower.endsWith(".vue")) return "vue";
|
|
1043
|
+
if (lower.endsWith(".ts")) return "typescript";
|
|
1044
|
+
if (lower.endsWith(".tsx")) return "typescript";
|
|
1045
|
+
if (lower.endsWith(".js")) return "javascript";
|
|
1046
|
+
if (lower.endsWith(".jsx")) return "javascript";
|
|
1047
|
+
return "typescript";
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
// src/PlaygroundRender.tsx
|
|
1051
|
+
import React4 from "react";
|
|
1052
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1053
|
+
var PlaygroundRender = ({ height = "100%", className, style }) => {
|
|
1054
|
+
const { isCompiling, error, renderedComponent, renderedDomModule, runtime } = usePlayground();
|
|
1055
|
+
return /* @__PURE__ */ jsxs3(
|
|
1056
|
+
"div",
|
|
1057
|
+
{
|
|
1058
|
+
className,
|
|
1059
|
+
style: {
|
|
1060
|
+
border: "1px solid #e2e8f0",
|
|
1061
|
+
borderRadius: 12,
|
|
1062
|
+
padding: 12,
|
|
1063
|
+
overflow: "auto",
|
|
1064
|
+
position: "relative",
|
|
1065
|
+
height,
|
|
1066
|
+
...style
|
|
1067
|
+
},
|
|
1068
|
+
children: [
|
|
1069
|
+
isCompiling && /* @__PURE__ */ jsx4(StatusBanner, { message: "Compiling...", tone: "info" }),
|
|
1070
|
+
error && /* @__PURE__ */ jsx4(StatusBanner, { message: error, tone: "error" }),
|
|
1071
|
+
!error && !isCompiling && runtime === "react" && renderedComponent && /* @__PURE__ */ jsx4(RenderSurface, { component: renderedComponent }),
|
|
1072
|
+
!error && !isCompiling && runtime === "dom" && renderedDomModule && /* @__PURE__ */ jsx4(DomRenderSurface, { module: renderedDomModule })
|
|
1073
|
+
]
|
|
1074
|
+
}
|
|
1075
|
+
);
|
|
1076
|
+
};
|
|
1077
|
+
var RenderSurface = ({ component: Component }) => {
|
|
1078
|
+
return /* @__PURE__ */ jsx4("div", { style: { width: "100%", height: "100%", color: "#0f172a" }, children: /* @__PURE__ */ jsx4(Component, {}) });
|
|
1079
|
+
};
|
|
1080
|
+
var DomRenderSurface = ({ module }) => {
|
|
1081
|
+
const ref = React4.useRef(null);
|
|
1082
|
+
React4.useEffect(() => {
|
|
1083
|
+
if (!ref.current) return;
|
|
1084
|
+
const el = ref.current;
|
|
1085
|
+
const result = module.mount(el);
|
|
1086
|
+
return () => {
|
|
1087
|
+
if (typeof module.unmount === "function") {
|
|
1088
|
+
module.unmount();
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
if (typeof result === "function") {
|
|
1092
|
+
result();
|
|
1093
|
+
}
|
|
1094
|
+
el.replaceChildren();
|
|
1095
|
+
};
|
|
1096
|
+
}, [module]);
|
|
1097
|
+
return /* @__PURE__ */ jsx4("div", { ref, style: { width: "100%", height: "100%" } });
|
|
1098
|
+
};
|
|
1099
|
+
var StatusBanner = ({ message, tone }) => {
|
|
1100
|
+
const colors = tone === "error" ? { background: "#fef2f2", border: "#fecdd3", text: "#991b1b" } : { background: "#eff6ff", border: "#bfdbfe", text: "#1d4ed8" };
|
|
1101
|
+
return /* @__PURE__ */ jsx4(
|
|
1102
|
+
"div",
|
|
1103
|
+
{
|
|
1104
|
+
style: {
|
|
1105
|
+
background: colors.background,
|
|
1106
|
+
border: `1px solid ${colors.border}`,
|
|
1107
|
+
color: colors.text,
|
|
1108
|
+
padding: "10px 12px",
|
|
1109
|
+
borderRadius: 10,
|
|
1110
|
+
fontSize: 14,
|
|
1111
|
+
marginBottom: 8
|
|
1112
|
+
},
|
|
1113
|
+
children: message
|
|
1114
|
+
}
|
|
1115
|
+
);
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
// src/Playground.tsx
|
|
1119
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1120
|
+
var Playground = ({
|
|
1121
|
+
initialCode = defaultSnippet,
|
|
1122
|
+
initialFiles,
|
|
1123
|
+
entryFile,
|
|
1124
|
+
plugins = [],
|
|
1125
|
+
dependencies,
|
|
1126
|
+
height = "80vh",
|
|
1127
|
+
showFileTree,
|
|
1128
|
+
fileTreeDefaultOpen,
|
|
1129
|
+
fileTreeWidth,
|
|
1130
|
+
mode = "code",
|
|
1131
|
+
formValue,
|
|
1132
|
+
onFormValueChange,
|
|
1133
|
+
renderForm
|
|
1134
|
+
}) => {
|
|
1135
|
+
const layoutStyle = useMemo3(
|
|
1136
|
+
() => ({
|
|
1137
|
+
display: "grid",
|
|
1138
|
+
gridTemplateColumns: mode === "form" ? "360px 1fr" : "1fr 1fr",
|
|
1139
|
+
gap: "12px",
|
|
1140
|
+
height,
|
|
1141
|
+
width: "100%"
|
|
1142
|
+
}),
|
|
1143
|
+
[height, mode]
|
|
1144
|
+
);
|
|
1145
|
+
return /* @__PURE__ */ jsx5(
|
|
1146
|
+
PlaygroundProvider,
|
|
1147
|
+
{
|
|
1148
|
+
initialCode,
|
|
1149
|
+
initialFiles,
|
|
1150
|
+
entryFile,
|
|
1151
|
+
plugins,
|
|
1152
|
+
dependencies,
|
|
1153
|
+
formValue,
|
|
1154
|
+
onFormValueChange,
|
|
1155
|
+
children: /* @__PURE__ */ jsxs4("div", { style: layoutStyle, children: [
|
|
1156
|
+
mode === "form" ? renderForm ? renderForm({ value: formValue, onChange: onFormValueChange }) : /* @__PURE__ */ jsx5(FormPlaceholder, {}) : /* @__PURE__ */ jsx5(
|
|
1157
|
+
PlaygroundEditor,
|
|
1158
|
+
{
|
|
1159
|
+
showFileTree,
|
|
1160
|
+
fileTreeDefaultOpen,
|
|
1161
|
+
fileTreeWidth
|
|
1162
|
+
}
|
|
1163
|
+
),
|
|
1164
|
+
/* @__PURE__ */ jsx5(PlaygroundRender, {})
|
|
1165
|
+
] })
|
|
1166
|
+
}
|
|
1167
|
+
);
|
|
1168
|
+
};
|
|
1169
|
+
var FormPlaceholder = () => {
|
|
1170
|
+
return /* @__PURE__ */ jsxs4(
|
|
1171
|
+
"div",
|
|
1172
|
+
{
|
|
1173
|
+
style: {
|
|
1174
|
+
border: "1px solid #e2e8f0",
|
|
1175
|
+
borderRadius: 12,
|
|
1176
|
+
padding: 12,
|
|
1177
|
+
background: "#fff",
|
|
1178
|
+
color: "#0f172a",
|
|
1179
|
+
overflow: "auto"
|
|
1180
|
+
},
|
|
1181
|
+
children: [
|
|
1182
|
+
/* @__PURE__ */ jsx5("div", { style: { fontWeight: 700, marginBottom: 6 }, children: "Form mode" }),
|
|
1183
|
+
/* @__PURE__ */ jsxs4("div", { style: { fontSize: 13, color: "#475569", lineHeight: 1.5 }, children: [
|
|
1184
|
+
"Provide ",
|
|
1185
|
+
/* @__PURE__ */ jsx5("code", { children: "renderForm" }),
|
|
1186
|
+
" to render your own form UI and pass values via ",
|
|
1187
|
+
/* @__PURE__ */ jsx5("code", { children: "formValue" }),
|
|
1188
|
+
"."
|
|
1189
|
+
] })
|
|
1190
|
+
]
|
|
1191
|
+
}
|
|
1192
|
+
);
|
|
1193
|
+
};
|
|
1194
|
+
export {
|
|
1195
|
+
Playground,
|
|
1196
|
+
PlaygroundEditor,
|
|
1197
|
+
PlaygroundFileTree,
|
|
1198
|
+
PlaygroundProvider,
|
|
1199
|
+
PlaygroundRender,
|
|
1200
|
+
bundleWithRollup,
|
|
1201
|
+
compileUserCode,
|
|
1202
|
+
compileVirtualFiles,
|
|
1203
|
+
defaultSnippet,
|
|
1204
|
+
usePlayground
|
|
1205
|
+
};
|
|
1206
|
+
//# sourceMappingURL=index.mjs.map
|