@ttsc/playground 0.14.0-dev.20260528.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.
Files changed (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +230 -0
  3. package/lib/src/compiler/DEFAULT_ENTRY_FILE.d.ts +2 -0
  4. package/lib/src/compiler/DEFAULT_ENTRY_FILE.js +6 -0
  5. package/lib/src/compiler/DEFAULT_ENTRY_FILE.js.map +1 -0
  6. package/lib/src/compiler/DEFAULT_LINT_PLUGIN_NAME.d.ts +5 -0
  7. package/lib/src/compiler/DEFAULT_LINT_PLUGIN_NAME.js +9 -0
  8. package/lib/src/compiler/DEFAULT_LINT_PLUGIN_NAME.js.map +1 -0
  9. package/lib/src/compiler/DEFAULT_PLAYGROUND_COMPILER_OPTIONS.d.ts +19 -0
  10. package/lib/src/compiler/DEFAULT_PLAYGROUND_COMPILER_OPTIONS.js +23 -0
  11. package/lib/src/compiler/DEFAULT_PLAYGROUND_COMPILER_OPTIONS.js.map +1 -0
  12. package/lib/src/compiler/DEFAULT_TSCONFIG_PATH.d.ts +2 -0
  13. package/lib/src/compiler/DEFAULT_TSCONFIG_PATH.js +6 -0
  14. package/lib/src/compiler/DEFAULT_TSCONFIG_PATH.js.map +1 -0
  15. package/lib/src/compiler/DEFAULT_TYPIA_PLUGIN_NAME.d.ts +6 -0
  16. package/lib/src/compiler/DEFAULT_TYPIA_PLUGIN_NAME.js +10 -0
  17. package/lib/src/compiler/DEFAULT_TYPIA_PLUGIN_NAME.js.map +1 -0
  18. package/lib/src/compiler/DEFAULT_WORK_DIR.d.ts +2 -0
  19. package/lib/src/compiler/DEFAULT_WORK_DIR.js +6 -0
  20. package/lib/src/compiler/DEFAULT_WORK_DIR.js.map +1 -0
  21. package/lib/src/compiler/buildTsconfigJSON.d.ts +9 -0
  22. package/lib/src/compiler/buildTsconfigJSON.js +24 -0
  23. package/lib/src/compiler/buildTsconfigJSON.js.map +1 -0
  24. package/lib/src/compiler/createTypiaSourcePackMount.d.ts +11 -0
  25. package/lib/src/compiler/createTypiaSourcePackMount.js +29 -0
  26. package/lib/src/compiler/createTypiaSourcePackMount.js.map +1 -0
  27. package/lib/src/compiler/createWorkerCompiler.d.ts +25 -0
  28. package/lib/src/compiler/createWorkerCompiler.js +227 -0
  29. package/lib/src/compiler/createWorkerCompiler.js.map +1 -0
  30. package/lib/src/compiler/installDependenciesIntoMemFS.d.ts +12 -0
  31. package/lib/src/compiler/installDependenciesIntoMemFS.js +25 -0
  32. package/lib/src/compiler/installDependenciesIntoMemFS.js.map +1 -0
  33. package/lib/src/compiler/installTypiaSourcePack.d.ts +11 -0
  34. package/lib/src/compiler/installTypiaSourcePack.js +21 -0
  35. package/lib/src/compiler/installTypiaSourcePack.js.map +1 -0
  36. package/lib/src/compiler/internal/joinUnder.d.ts +2 -0
  37. package/lib/src/compiler/internal/joinUnder.js +10 -0
  38. package/lib/src/compiler/internal/joinUnder.js.map +1 -0
  39. package/lib/src/compiler/internal/parseLintDiagnostics.d.ts +6 -0
  40. package/lib/src/compiler/internal/parseLintDiagnostics.js +57 -0
  41. package/lib/src/compiler/internal/parseLintDiagnostics.js.map +1 -0
  42. package/lib/src/compiler/internal/safeParseTypiaTransform.d.ts +11 -0
  43. package/lib/src/compiler/internal/safeParseTypiaTransform.js +21 -0
  44. package/lib/src/compiler/internal/safeParseTypiaTransform.js.map +1 -0
  45. package/lib/src/compiler/lineColumnOf.d.ts +5 -0
  46. package/lib/src/compiler/lineColumnOf.js +15 -0
  47. package/lib/src/compiler/lineColumnOf.js.map +1 -0
  48. package/lib/src/compiler/loadTypiaSourcePack.d.ts +8 -0
  49. package/lib/src/compiler/loadTypiaSourcePack.js +33 -0
  50. package/lib/src/compiler/loadTypiaSourcePack.js.map +1 -0
  51. package/lib/src/compiler/mapDiagnostic.d.ts +9 -0
  52. package/lib/src/compiler/mapDiagnostic.js +22 -0
  53. package/lib/src/compiler/mapDiagnostic.js.map +1 -0
  54. package/lib/src/compiler/normalizeError.d.ts +2 -0
  55. package/lib/src/compiler/normalizeError.js +14 -0
  56. package/lib/src/compiler/normalizeError.js.map +1 -0
  57. package/lib/src/compiler/normalizeNodeModulePath.d.ts +8 -0
  58. package/lib/src/compiler/normalizeNodeModulePath.js +19 -0
  59. package/lib/src/compiler/normalizeNodeModulePath.js.map +1 -0
  60. package/lib/src/compiler/pickEmittedJS.d.ts +6 -0
  61. package/lib/src/compiler/pickEmittedJS.js +26 -0
  62. package/lib/src/compiler/pickEmittedJS.js.map +1 -0
  63. package/lib/src/index.d.ts +47 -0
  64. package/lib/src/index.js +104 -0
  65. package/lib/src/index.js.map +1 -0
  66. package/lib/src/npm/BUILT_IN_PLAYGROUND_PACKAGES.d.ts +6 -0
  67. package/lib/src/npm/BUILT_IN_PLAYGROUND_PACKAGES.js +15 -0
  68. package/lib/src/npm/BUILT_IN_PLAYGROUND_PACKAGES.js.map +1 -0
  69. package/lib/src/npm/collectExternalPackageNames.d.ts +5 -0
  70. package/lib/src/npm/collectExternalPackageNames.js +34 -0
  71. package/lib/src/npm/collectExternalPackageNames.js.map +1 -0
  72. package/lib/src/npm/installPlaygroundDependencies.d.ts +13 -0
  73. package/lib/src/npm/installPlaygroundDependencies.js +121 -0
  74. package/lib/src/npm/installPlaygroundDependencies.js.map +1 -0
  75. package/lib/src/npm/internal/npmRegistry.d.ts +57 -0
  76. package/lib/src/npm/internal/npmRegistry.js +216 -0
  77. package/lib/src/npm/internal/npmRegistry.js.map +1 -0
  78. package/lib/src/npm/packageNameFromSpecifier.d.ts +7 -0
  79. package/lib/src/npm/packageNameFromSpecifier.js +66 -0
  80. package/lib/src/npm/packageNameFromSpecifier.js.map +1 -0
  81. package/lib/src/react/ConsoleViewer.d.ts +7 -0
  82. package/lib/src/react/ConsoleViewer.js +57 -0
  83. package/lib/src/react/ConsoleViewer.js.map +1 -0
  84. package/lib/src/react/DEFAULT_OPTION_TOGGLES.d.ts +3 -0
  85. package/lib/src/react/DEFAULT_OPTION_TOGGLES.js +17 -0
  86. package/lib/src/react/DEFAULT_OPTION_TOGGLES.js.map +1 -0
  87. package/lib/src/react/DependencyProgressModal.d.ts +7 -0
  88. package/lib/src/react/DependencyProgressModal.js +16 -0
  89. package/lib/src/react/DependencyProgressModal.js.map +1 -0
  90. package/lib/src/react/DiagnosticsPanel.d.ts +4 -0
  91. package/lib/src/react/DiagnosticsPanel.js +15 -0
  92. package/lib/src/react/DiagnosticsPanel.js.map +1 -0
  93. package/lib/src/react/ExamplePicker.d.ts +9 -0
  94. package/lib/src/react/ExamplePicker.js +43 -0
  95. package/lib/src/react/ExamplePicker.js.map +1 -0
  96. package/lib/src/react/LintPane.d.ts +9 -0
  97. package/lib/src/react/LintPane.js +17 -0
  98. package/lib/src/react/LintPane.js.map +1 -0
  99. package/lib/src/react/OptionsPanel.d.ts +13 -0
  100. package/lib/src/react/OptionsPanel.js +43 -0
  101. package/lib/src/react/OptionsPanel.js.map +1 -0
  102. package/lib/src/react/PlaygroundShell.d.ts +2 -0
  103. package/lib/src/react/PlaygroundShell.js +609 -0
  104. package/lib/src/react/PlaygroundShell.js.map +1 -0
  105. package/lib/src/react/ResultViewer.d.ts +10 -0
  106. package/lib/src/react/ResultViewer.js +49 -0
  107. package/lib/src/react/ResultViewer.js.map +1 -0
  108. package/lib/src/react/SourceEditor.d.ts +2 -0
  109. package/lib/src/react/SourceEditor.js +74 -0
  110. package/lib/src/react/SourceEditor.js.map +1 -0
  111. package/lib/src/react/createCompilerClient.d.ts +14 -0
  112. package/lib/src/react/createCompilerClient.js +77 -0
  113. package/lib/src/react/createCompilerClient.js.map +1 -0
  114. package/lib/src/sandbox/createSandboxRequire.d.ts +11 -0
  115. package/lib/src/sandbox/createSandboxRequire.js +211 -0
  116. package/lib/src/sandbox/createSandboxRequire.js.map +1 -0
  117. package/lib/src/sandbox/loadTypiaRuntimePack.d.ts +7 -0
  118. package/lib/src/sandbox/loadTypiaRuntimePack.js +35 -0
  119. package/lib/src/sandbox/loadTypiaRuntimePack.js.map +1 -0
  120. package/lib/src/structures/IBuildTsconfigOptions.d.ts +19 -0
  121. package/lib/src/structures/IBuildTsconfigOptions.js +3 -0
  122. package/lib/src/structures/IBuildTsconfigOptions.js.map +1 -0
  123. package/lib/src/structures/ICompilerService.d.ts +83 -0
  124. package/lib/src/structures/ICompilerService.js +3 -0
  125. package/lib/src/structures/ICompilerService.js.map +1 -0
  126. package/lib/src/structures/IConsoleMessage.d.ts +10 -0
  127. package/lib/src/structures/IConsoleMessage.js +3 -0
  128. package/lib/src/structures/IConsoleMessage.js.map +1 -0
  129. package/lib/src/structures/ICreateCompilerClientOptions.d.ts +13 -0
  130. package/lib/src/structures/ICreateCompilerClientOptions.js +3 -0
  131. package/lib/src/structures/ICreateCompilerClientOptions.js.map +1 -0
  132. package/lib/src/structures/ICreateWorkerCompilerOptions.d.ts +35 -0
  133. package/lib/src/structures/ICreateWorkerCompilerOptions.js +3 -0
  134. package/lib/src/structures/ICreateWorkerCompilerOptions.js.map +1 -0
  135. package/lib/src/structures/IInstallTypiaSourcePackOptions.d.ts +15 -0
  136. package/lib/src/structures/IInstallTypiaSourcePackOptions.js +3 -0
  137. package/lib/src/structures/IInstallTypiaSourcePackOptions.js.map +1 -0
  138. package/lib/src/structures/ILintPluginConfig.d.ts +5 -0
  139. package/lib/src/structures/ILintPluginConfig.js +3 -0
  140. package/lib/src/structures/ILintPluginConfig.js.map +1 -0
  141. package/lib/src/structures/IOptionToggle.d.ts +12 -0
  142. package/lib/src/structures/IOptionToggle.js +3 -0
  143. package/lib/src/structures/IOptionToggle.js.map +1 -0
  144. package/lib/src/structures/IPlaygroundDependencyInstallOptions.d.ts +16 -0
  145. package/lib/src/structures/IPlaygroundDependencyInstallOptions.js +3 -0
  146. package/lib/src/structures/IPlaygroundDependencyInstallOptions.js.map +1 -0
  147. package/lib/src/structures/IPlaygroundDependencyInstallResult.d.ts +20 -0
  148. package/lib/src/structures/IPlaygroundDependencyInstallResult.js +3 -0
  149. package/lib/src/structures/IPlaygroundDependencyInstallResult.js.map +1 -0
  150. package/lib/src/structures/IPlaygroundDependencyPackage.d.ts +8 -0
  151. package/lib/src/structures/IPlaygroundDependencyPackage.js +3 -0
  152. package/lib/src/structures/IPlaygroundDependencyPackage.js.map +1 -0
  153. package/lib/src/structures/IPlaygroundDependencyProgress.d.ts +10 -0
  154. package/lib/src/structures/IPlaygroundDependencyProgress.js +3 -0
  155. package/lib/src/structures/IPlaygroundDependencyProgress.js.map +1 -0
  156. package/lib/src/structures/IPlaygroundDependencyProgressPhase.d.ts +2 -0
  157. package/lib/src/structures/IPlaygroundDependencyProgressPhase.js +3 -0
  158. package/lib/src/structures/IPlaygroundDependencyProgressPhase.js.map +1 -0
  159. package/lib/src/structures/IPlaygroundExample.d.ts +16 -0
  160. package/lib/src/structures/IPlaygroundExample.js +3 -0
  161. package/lib/src/structures/IPlaygroundExample.js.map +1 -0
  162. package/lib/src/structures/IPlaygroundShellProps.d.ts +64 -0
  163. package/lib/src/structures/IPlaygroundShellProps.js +3 -0
  164. package/lib/src/structures/IPlaygroundShellProps.js.map +1 -0
  165. package/lib/src/structures/ISourceEditorProps.d.ts +18 -0
  166. package/lib/src/structures/ISourceEditorProps.js +3 -0
  167. package/lib/src/structures/ISourceEditorProps.js.map +1 -0
  168. package/lib/src/structures/ITransformOptions.d.ts +15 -0
  169. package/lib/src/structures/ITransformOptions.js +3 -0
  170. package/lib/src/structures/ITransformOptions.js.map +1 -0
  171. package/lib/src/structures/ITypiaPluginConfig.d.ts +19 -0
  172. package/lib/src/structures/ITypiaPluginConfig.js +3 -0
  173. package/lib/src/structures/ITypiaPluginConfig.js.map +1 -0
  174. package/lib/src/structures/index.d.ts +18 -0
  175. package/lib/src/structures/index.js +35 -0
  176. package/lib/src/structures/index.js.map +1 -0
  177. package/package.json +66 -0
  178. package/src/compiler/DEFAULT_ENTRY_FILE.ts +2 -0
  179. package/src/compiler/DEFAULT_LINT_PLUGIN_NAME.ts +5 -0
  180. package/src/compiler/DEFAULT_PLAYGROUND_COMPILER_OPTIONS.ts +19 -0
  181. package/src/compiler/DEFAULT_TSCONFIG_PATH.ts +2 -0
  182. package/src/compiler/DEFAULT_TYPIA_PLUGIN_NAME.ts +6 -0
  183. package/src/compiler/DEFAULT_WORK_DIR.ts +2 -0
  184. package/src/compiler/buildTsconfigJSON.ts +22 -0
  185. package/src/compiler/createTypiaSourcePackMount.ts +32 -0
  186. package/src/compiler/createWorkerCompiler.ts +274 -0
  187. package/src/compiler/installDependenciesIntoMemFS.ts +28 -0
  188. package/src/compiler/installTypiaSourcePack.ts +24 -0
  189. package/src/compiler/internal/joinUnder.ts +5 -0
  190. package/src/compiler/internal/parseLintDiagnostics.ts +61 -0
  191. package/src/compiler/internal/safeParseTypiaTransform.ts +24 -0
  192. package/src/compiler/lineColumnOf.ts +14 -0
  193. package/src/compiler/loadTypiaSourcePack.ts +37 -0
  194. package/src/compiler/mapDiagnostic.ts +26 -0
  195. package/src/compiler/normalizeError.ts +12 -0
  196. package/src/compiler/normalizeNodeModulePath.ts +13 -0
  197. package/src/compiler/pickEmittedJS.ts +23 -0
  198. package/src/index.ts +57 -0
  199. package/src/npm/BUILT_IN_PLAYGROUND_PACKAGES.ts +11 -0
  200. package/src/npm/collectExternalPackageNames.ts +34 -0
  201. package/src/npm/installPlaygroundDependencies.ts +163 -0
  202. package/src/npm/internal/npmRegistry.ts +288 -0
  203. package/src/npm/packageNameFromSpecifier.ts +61 -0
  204. package/src/react/ConsoleViewer.tsx +121 -0
  205. package/src/react/DEFAULT_OPTION_TOGGLES.ts +15 -0
  206. package/src/react/DependencyProgressModal.tsx +73 -0
  207. package/src/react/DiagnosticsPanel.tsx +74 -0
  208. package/src/react/ExamplePicker.tsx +93 -0
  209. package/src/react/LintPane.tsx +58 -0
  210. package/src/react/OptionsPanel.tsx +132 -0
  211. package/src/react/PlaygroundShell.tsx +864 -0
  212. package/src/react/ResultViewer.tsx +75 -0
  213. package/src/react/SourceEditor.tsx +96 -0
  214. package/src/react/createCompilerClient.ts +79 -0
  215. package/src/sandbox/createSandboxRequire.ts +275 -0
  216. package/src/sandbox/loadTypiaRuntimePack.ts +36 -0
  217. package/src/structures/IBuildTsconfigOptions.ts +19 -0
  218. package/src/structures/ICompilerService.ts +95 -0
  219. package/src/structures/IConsoleMessage.ts +10 -0
  220. package/src/structures/ICreateCompilerClientOptions.ts +13 -0
  221. package/src/structures/ICreateWorkerCompilerOptions.ts +40 -0
  222. package/src/structures/IInstallTypiaSourcePackOptions.ts +15 -0
  223. package/src/structures/ILintPluginConfig.ts +5 -0
  224. package/src/structures/IOptionToggle.ts +12 -0
  225. package/src/structures/IPlaygroundDependencyInstallOptions.ts +17 -0
  226. package/src/structures/IPlaygroundDependencyInstallResult.ts +21 -0
  227. package/src/structures/IPlaygroundDependencyPackage.ts +8 -0
  228. package/src/structures/IPlaygroundDependencyProgress.ts +11 -0
  229. package/src/structures/IPlaygroundDependencyProgressPhase.ts +9 -0
  230. package/src/structures/IPlaygroundExample.ts +16 -0
  231. package/src/structures/IPlaygroundShellProps.ts +77 -0
  232. package/src/structures/ISourceEditorProps.ts +18 -0
  233. package/src/structures/ITransformOptions.ts +15 -0
  234. package/src/structures/ITypiaPluginConfig.ts +20 -0
  235. package/src/structures/index.ts +18 -0
@@ -0,0 +1,864 @@
1
+ "use client";
2
+
3
+ import {
4
+ compressToEncodedURIComponent,
5
+ decompressFromEncodedURIComponent,
6
+ } from "lz-string";
7
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
8
+
9
+ import { BUILT_IN_PLAYGROUND_PACKAGES } from "../npm/BUILT_IN_PLAYGROUND_PACKAGES";
10
+ import { collectExternalPackageNames } from "../npm/collectExternalPackageNames";
11
+ import { installPlaygroundDependencies } from "../npm/installPlaygroundDependencies";
12
+ import type { ICompilerService } from "../structures/ICompilerService";
13
+ import type { IConsoleMessage } from "../structures/IConsoleMessage";
14
+ import type { IPlaygroundDependencyProgress } from "../structures/IPlaygroundDependencyProgress";
15
+ import type { IPlaygroundShellProps } from "../structures/IPlaygroundShellProps";
16
+ import type { ITransformOptions } from "../structures/ITransformOptions";
17
+ import { ConsoleViewer } from "./ConsoleViewer";
18
+ import { createCompilerClient } from "./createCompilerClient";
19
+ import { DEFAULT_OPTION_TOGGLES } from "./DEFAULT_OPTION_TOGGLES";
20
+ import { DependencyProgressModal } from "./DependencyProgressModal";
21
+ import { DiagnosticsPanel } from "./DiagnosticsPanel";
22
+ import { ExamplePicker } from "./ExamplePicker";
23
+ import { LintPane } from "./LintPane";
24
+ import { OptionsPanel } from "./OptionsPanel";
25
+ import { ResultViewer } from "./ResultViewer";
26
+ import { SourceEditor } from "./SourceEditor";
27
+
28
+ const DEFAULT_OPTIONS: ITransformOptions = {
29
+ typia: true,
30
+ lint: true,
31
+ };
32
+
33
+ const DEPENDENCY_INSTALL_QUIET_MS = 900;
34
+ const SHARE_URL_WARN_BYTES = 2000;
35
+
36
+ type Tab = "javascript" | "lint";
37
+
38
+ export function PlaygroundShell({
39
+ workerUrl,
40
+ defaultScript,
41
+ examples = [],
42
+ exampleGroupLabels,
43
+ optionToggles = DEFAULT_OPTION_TOGGLES,
44
+ defaultOptions = DEFAULT_OPTIONS,
45
+ staticEditorLibs,
46
+ preinstalledPackages = BUILT_IN_PLAYGROUND_PACKAGES,
47
+ executeBundle,
48
+ brand,
49
+ resultCaption = defaultResultCaption,
50
+ }: IPlaygroundShellProps) {
51
+ const client = useMemo(
52
+ () => createCompilerClient({ workerUrl }),
53
+ [workerUrl],
54
+ );
55
+ const createCompilerService = client.connect;
56
+
57
+ const [source, setSource] = useState<string>(defaultScript);
58
+ const [target, setTarget] = useState<Tab>("javascript");
59
+ const [options, setOptions] = useState<ITransformOptions>(defaultOptions);
60
+ const [result, setResult] = useState<ICompilerService.IResult | null>(null);
61
+ const [lintDiagnostics, setLintDiagnostics] = useState<
62
+ ICompilerService.IDiagnostic[]
63
+ >([]);
64
+ const [optionsOpen, setOptionsOpen] = useState(false);
65
+ const [running, setRunning] = useState(false);
66
+ const [shareToast, setShareToast] = useState(false);
67
+ const [consoleMessages, setConsoleMessages] = useState<IConsoleMessage[]>(
68
+ [],
69
+ );
70
+ const [executing, setExecuting] = useState(false);
71
+ const [bootError, setBootError] = useState<unknown>(null);
72
+ const [bootPhase, setBootPhase] = useState<"booting" | "ready" | "failed">(
73
+ "booting",
74
+ );
75
+ const [bundleError, setBundleError] = useState<string | null>(null);
76
+ const [shareWarn, setShareWarn] = useState<string | null>(null);
77
+ const [sourceFromURL, setSourceFromURL] = useState(false);
78
+ const [editorExtraLibs, setEditorExtraLibs] = useState<
79
+ Record<string, string>
80
+ >({});
81
+ const [dependencyProgress, setDependencyProgress] =
82
+ useState<IPlaygroundDependencyProgress | null>(null);
83
+ const [dependencyPackageNames, setDependencyPackageNames] = useState<
84
+ string[]
85
+ >([]);
86
+ const debounce = useRef<number | null>(null);
87
+ const shareToastTimer = useRef<number | null>(null);
88
+ const dependencyProgressTimer = useRef<number | null>(null);
89
+ const dependencyInstallChain = useRef<Promise<void>>(Promise.resolve());
90
+ const dependencyAbort = useRef<AbortController | null>(null);
91
+ // Mirror `preinstalledPackages` into a ref so the worker-teardown
92
+ // effect can read the current value without taking the prop as a
93
+ // dep — listing the array prop in deps would tear down the worker on
94
+ // every parent re-render that produces a fresh array reference.
95
+ const preinstalledPackagesRef =
96
+ useRef<readonly string[]>(preinstalledPackages);
97
+ // The ref tracks names the wasm MemFS already has — preinstalled at boot
98
+ // (via `preinstalledPackages`) plus everything `installPlaygroundDependencies`
99
+ // has added across the session. A useEffect below merges fresh
100
+ // preinstalledPackages prop values into the ref so a parent that swaps
101
+ // the prop later does not race a now-stale Set.
102
+ const installedDependencyNames = useRef<Set<string>>(
103
+ new Set<string>(preinstalledPackages),
104
+ );
105
+ // Accumulated runtime-file map produced by every successful
106
+ // installPlaygroundDependencies call. Threaded through to executeBundle so
107
+ // the in-page Execute sandbox's require can resolve any npm package the
108
+ // user installed (without it, `import {v4} from "uuid"` compiles fine but
109
+ // Execute throws because the worker mounts uuid into the wasm MemFS only).
110
+ const runtimeDependencyFiles = useRef<Record<string, string>>({});
111
+ const sourceVersion = useRef(0);
112
+ const latestSource = useRef(source);
113
+ // Race guards: each pipeline (compile/run vs Execute) owns its own epoch.
114
+ // Sharing one epoch would make a fresh Execute click invalidate an in-
115
+ // flight compile (and vice versa) and leave the spinner/button flags
116
+ // stuck because the older pipeline's finally would see a stale epoch.
117
+ // `updateSource` bumps the COMPILE epoch only — typing aborts a stale
118
+ // compile but does not interrupt a click-driven Execute.
119
+ const compileEpoch = useRef(0);
120
+ const executeEpoch = useRef(0);
121
+ // Retry epoch: each Retry click bumps it; only the latest retry's
122
+ // async block is allowed to write boot state. Prior retries' bodies
123
+ // bail on epoch mismatch so a fast double-Retry can't leave a stale
124
+ // `setBootPhase("ready")` chasing a torn-down connection.
125
+ const retryEpoch = useRef(0);
126
+
127
+ const mergedExtraLibs = useMemo(
128
+ () => ({ ...staticEditorLibs, ...editorExtraLibs }),
129
+ [staticEditorLibs, editorExtraLibs],
130
+ );
131
+
132
+ const updateSource = useCallback((next: string) => {
133
+ sourceVersion.current++;
134
+ latestSource.current = next;
135
+ // Bump BOTH epochs: typing must abort an in-flight compile (the
136
+ // result would be stale) AND an in-flight Execute (the bundled code
137
+ // would not match what's on screen). Without bumping executeEpoch,
138
+ // an Execute whose dependency install was aborted by this typing
139
+ // would silently treat the abort as success and bundle against an
140
+ // incomplete MemFS — emitting JS for packages the user just edited
141
+ // away from, or failing in confusing ways.
142
+ compileEpoch.current++;
143
+ executeEpoch.current++;
144
+ dependencyAbort.current?.abort(createAbortError("source changed"));
145
+ setDependencyProgress(null);
146
+ setDependencyPackageNames([]);
147
+ setSource(next);
148
+ }, []);
149
+
150
+ // ── Sync installedDependencyNames + preinstalledPackagesRef on prop change ──
151
+ // The refs capture the initial value on mount; without this effect a
152
+ // parent that swaps `preinstalledPackages` later would race a stale
153
+ // Set against the fresh prop used in `ignoredPackages` below.
154
+ useEffect(() => {
155
+ preinstalledPackagesRef.current = preinstalledPackages;
156
+ for (const name of preinstalledPackages) {
157
+ installedDependencyNames.current.add(name);
158
+ }
159
+ }, [preinstalledPackages]);
160
+
161
+ // ── Decode source from URL on mount ──
162
+ useEffect(() => {
163
+ const params = new URLSearchParams(window.location.search);
164
+ const encoded = params.get("script");
165
+ if (encoded) {
166
+ const decoded = decompressFromEncodedURIComponent(encoded);
167
+ if (decoded) {
168
+ updateSource(decoded);
169
+ setSourceFromURL(true);
170
+ }
171
+ }
172
+ }, [updateSource]);
173
+
174
+ // ── Eagerly boot the worker so first compile is instant ──
175
+ useEffect(() => {
176
+ let cancelled = false;
177
+ setBootPhase("booting");
178
+ createCompilerService().then(
179
+ () => {
180
+ if (!cancelled) setBootPhase("ready");
181
+ },
182
+ (err: unknown) => {
183
+ if (!cancelled) {
184
+ setBootError(err);
185
+ setBootPhase("failed");
186
+ }
187
+ },
188
+ );
189
+ return () => {
190
+ cancelled = true;
191
+ };
192
+ }, [createCompilerService]);
193
+
194
+ const installDependenciesForSource = useCallback(
195
+ async (
196
+ input: string,
197
+ version: number = sourceVersion.current,
198
+ ): Promise<unknown | null> => {
199
+ const task = dependencyInstallChain.current.then(async () => {
200
+ const firstPassPackageNames = collectExternalPackageNames(
201
+ input,
202
+ preinstalledPackages,
203
+ );
204
+ const firstPassMissing = firstPassPackageNames.filter(
205
+ (name) => !installedDependencyNames.current.has(name),
206
+ );
207
+ if (firstPassMissing.length === 0) return null;
208
+
209
+ await wait(DEPENDENCY_INSTALL_QUIET_MS);
210
+ if (sourceVersion.current !== version) return null;
211
+
212
+ const packageNames = collectExternalPackageNames(
213
+ latestSource.current,
214
+ preinstalledPackages,
215
+ );
216
+ const missing = packageNames.filter(
217
+ (name) => !installedDependencyNames.current.has(name),
218
+ );
219
+ if (missing.length === 0) return null;
220
+
221
+ if (dependencyProgressTimer.current !== null) {
222
+ window.clearTimeout(dependencyProgressTimer.current);
223
+ dependencyProgressTimer.current = null;
224
+ }
225
+ setDependencyPackageNames(missing);
226
+ const abort = new AbortController();
227
+ dependencyAbort.current = abort;
228
+ try {
229
+ const installed = await installPlaygroundDependencies(missing, {
230
+ installedPackages: installedDependencyNames.current,
231
+ ignoredPackages: preinstalledPackages,
232
+ signal: abort.signal,
233
+ onProgress: setDependencyProgress,
234
+ });
235
+ if (sourceVersion.current !== version) return null;
236
+ if (Object.keys(installed.compilerFiles).length > 0) {
237
+ const service = await createCompilerService();
238
+ await service.installDependencies({
239
+ files: installed.compilerFiles,
240
+ packages: installed.packages.map(({ name, version }) => ({
241
+ name,
242
+ version,
243
+ })),
244
+ });
245
+ }
246
+ for (const pkg of installed.packages) {
247
+ installedDependencyNames.current.add(pkg.name);
248
+ }
249
+ if (Object.keys(installed.editorLibs).length > 0) {
250
+ setEditorExtraLibs((prev) => ({
251
+ ...prev,
252
+ ...installed.editorLibs,
253
+ }));
254
+ }
255
+ // Accumulate runtime files so the in-page Execute sandbox can
256
+ // resolve every package the user installed across this session.
257
+ runtimeDependencyFiles.current = {
258
+ ...runtimeDependencyFiles.current,
259
+ ...installed.runtimeFiles,
260
+ };
261
+ dependencyProgressTimer.current = window.setTimeout(() => {
262
+ setDependencyProgress(null);
263
+ setDependencyPackageNames([]);
264
+ dependencyProgressTimer.current = null;
265
+ }, 350);
266
+ return null;
267
+ } catch (error) {
268
+ if (isAbortError(error)) {
269
+ setDependencyProgress(null);
270
+ setDependencyPackageNames([]);
271
+ return null;
272
+ }
273
+ setDependencyProgress({
274
+ phase: "error",
275
+ packageName: missing[0],
276
+ completed: 0,
277
+ total: missing.length,
278
+ message: describeUnknownError(error),
279
+ });
280
+ dependencyProgressTimer.current = window.setTimeout(() => {
281
+ setDependencyProgress(null);
282
+ setDependencyPackageNames([]);
283
+ dependencyProgressTimer.current = null;
284
+ }, 2400);
285
+ return error;
286
+ } finally {
287
+ if (dependencyAbort.current === abort)
288
+ dependencyAbort.current = null;
289
+ }
290
+ });
291
+ dependencyInstallChain.current = task.then(() => {});
292
+ return task;
293
+ },
294
+ [createCompilerService, preinstalledPackages],
295
+ );
296
+
297
+ // ── Run compile when source / options change ──
298
+ //
299
+ // `target` (the active tab) is intentionally NOT a trigger here: the
300
+ // compile produces the same result + lintDiagnostics regardless of
301
+ // which tab the user is looking at; the tab choice only swaps which
302
+ // pane is rendered. Re-running the wasm-heavy pipeline on every tab
303
+ // click would burn multiple seconds of work per click.
304
+ const run = useCallback(
305
+ async (
306
+ input: string,
307
+ opts: ITransformOptions,
308
+ version: number,
309
+ ) => {
310
+ const epoch = ++compileEpoch.current;
311
+ setRunning(true);
312
+ try {
313
+ const dependencyError = await installDependenciesForSource(
314
+ input,
315
+ version,
316
+ );
317
+ if (compileEpoch.current !== epoch) return;
318
+ if (dependencyError) {
319
+ setResult({
320
+ type: "error",
321
+ target: "javascript",
322
+ value: normalizeClientError(dependencyError),
323
+ });
324
+ // Keep prior lintDiagnostics intact — a dependency-install blip
325
+ // shouldn't wipe the user's most recent successful lint output.
326
+ return;
327
+ }
328
+ const service = await createCompilerService();
329
+ const next = await service.compile({
330
+ source: input,
331
+ options: opts,
332
+ });
333
+ if (compileEpoch.current !== epoch) return;
334
+ setResult(next);
335
+ if (opts.lint !== false) {
336
+ const lint = await service.lint({ source: input, options: opts });
337
+ if (compileEpoch.current !== epoch) return;
338
+ setLintDiagnostics(lint.diagnostics);
339
+ } else {
340
+ setLintDiagnostics([]);
341
+ }
342
+ } catch (err) {
343
+ if (compileEpoch.current !== epoch) return;
344
+ // Surface the error in the diagnostics pane via an error result —
345
+ // a transient compile/lint/install rejection (tgrid timeout,
346
+ // message-channel disconnect) must NOT tear the playground into
347
+ // the fatal boot-error screen and force a worker rebuild. Only
348
+ // the eager boot useEffect may flip bootPhase to "failed".
349
+ setResult({
350
+ type: "error",
351
+ target: "javascript",
352
+ value: normalizeClientError(err),
353
+ });
354
+ // Leave lintDiagnostics alone — clearing them on a transient
355
+ // compile blip would wipe the user's last good lint output.
356
+ } finally {
357
+ // Only the winning epoch clears the flag. Older pipelines that
358
+ // returned early on an epoch mismatch must NOT clear running, or
359
+ // a fresh in-flight compile would show "ready" while it's still
360
+ // working. compileEpoch is bumped only by updateSource and by the
361
+ // next run() — Execute uses its own executeEpoch — so this guard
362
+ // does not stick the spinner across pipeline boundaries.
363
+ if (compileEpoch.current === epoch) setRunning(false);
364
+ }
365
+ },
366
+ [createCompilerService, installDependenciesForSource],
367
+ );
368
+
369
+ useEffect(() => {
370
+ if (bootPhase !== "ready") return;
371
+ if (debounce.current !== null) window.clearTimeout(debounce.current);
372
+ const version = sourceVersion.current;
373
+ debounce.current = window.setTimeout(() => {
374
+ void run(source, options, version);
375
+ }, 280);
376
+ return () => {
377
+ if (debounce.current !== null) window.clearTimeout(debounce.current);
378
+ };
379
+ // `target` (the active tab) is intentionally NOT a dep — see the
380
+ // comment on `run` above. Re-running the wasm pipeline per tab
381
+ // click would burn seconds of work for an identical result.
382
+ }, [source, options, run, bootPhase]);
383
+
384
+ const onShare = useCallback(() => {
385
+ const url = new URL(window.location.href);
386
+ url.searchParams.set("script", compressToEncodedURIComponent(source));
387
+ const urlString = url.toString();
388
+ void navigator.clipboard.writeText(urlString);
389
+ window.history.replaceState(null, "", urlString);
390
+ setShareToast(true);
391
+ if (shareToastTimer.current !== null)
392
+ window.clearTimeout(shareToastTimer.current);
393
+ shareToastTimer.current = window.setTimeout(() => {
394
+ setShareToast(false);
395
+ shareToastTimer.current = null;
396
+ }, 1800);
397
+ if (urlString.length > SHARE_URL_WARN_BYTES) {
398
+ setShareWarn(
399
+ `Share URL is ${urlString.length} bytes — some browsers truncate URLs past ~2KB. Consider sharing as a Gist instead.`,
400
+ );
401
+ } else {
402
+ setShareWarn(null);
403
+ }
404
+ }, [source]);
405
+
406
+ useEffect(
407
+ () => () => {
408
+ if (shareToastTimer.current !== null)
409
+ window.clearTimeout(shareToastTimer.current);
410
+ if (dependencyProgressTimer.current !== null)
411
+ window.clearTimeout(dependencyProgressTimer.current);
412
+ dependencyAbort.current?.abort(createAbortError("playground unmounted"));
413
+ },
414
+ [],
415
+ );
416
+
417
+ // Tear down the Worker (+ its wasm instance) when the component
418
+ // unmounts or workerUrl changes. Without this an SPA navigation away
419
+ // from the playground leaks one Worker per mount; a workerUrl swap
420
+ // leaks the previous Worker forever.
421
+ //
422
+ // Reset the dependency-tracking refs too. They named packages that
423
+ // existed in the previous worker's MemFS; the fresh worker boots with
424
+ // only `preinstalledPackages` mounted, so carrying the old names
425
+ // would make installDependenciesForSource skip the install (because
426
+ // `installedDependencyNames.current.has(name)` is still true) and
427
+ // the next compile would fail with `Cannot find module`.
428
+ //
429
+ // The effect depends on `[client]` ONLY — listing the array prop
430
+ // `preinstalledPackages` in deps would tear down the worker on every
431
+ // parent re-render that produces a fresh array reference. We read the
432
+ // current value through `preinstalledPackagesRef`, which the sync
433
+ // effect above keeps up to date.
434
+ useEffect(
435
+ () => () => {
436
+ void client.reset();
437
+ installedDependencyNames.current = new Set<string>(
438
+ preinstalledPackagesRef.current,
439
+ );
440
+ runtimeDependencyFiles.current = {};
441
+ },
442
+ [client],
443
+ );
444
+
445
+ const onPickExample = useCallback(
446
+ (id: string) => {
447
+ const example = examples.find((e) => e.id === id);
448
+ if (example) {
449
+ updateSource(example.source);
450
+ setSourceFromURL(false);
451
+ }
452
+ },
453
+ [examples, updateSource],
454
+ );
455
+
456
+ const onReset = useCallback(() => {
457
+ updateSource(defaultScript);
458
+ setSourceFromURL(false);
459
+ }, [defaultScript, updateSource]);
460
+
461
+ const onExecute = useCallback(async () => {
462
+ if (!executeBundle) return;
463
+ // Bump the executeEpoch so a second Execute click invalidates the
464
+ // in-flight Execute. compileEpoch is unaffected — the user can edit
465
+ // the source mid-Execute without the running compile being torn down.
466
+ const epoch = ++executeEpoch.current;
467
+ setExecuting(true);
468
+ setBundleError(null);
469
+ // Clear the previous run's console output up front. Without this the
470
+ // pane keeps showing the old logs labeled as the new run until the
471
+ // first push fires — and an early-return bundle-error path (or an
472
+ // install rejection) might never push at all, leaving stale output
473
+ // attributed to the in-flight Execute.
474
+ setConsoleMessages([]);
475
+ const messages: IConsoleMessage[] = [];
476
+ const push = (type: IConsoleMessage["type"], args: unknown[]) => {
477
+ if (executeEpoch.current !== epoch) return;
478
+ messages.push({ type, value: args });
479
+ setConsoleMessages([...messages]);
480
+ };
481
+ try {
482
+ // Snapshot source + version atomically from the always-fresh refs.
483
+ // Reading `source` (React state) here can be stale within a batch
484
+ // — installDependenciesForSource would compare against the fresh
485
+ // ref and bail, while bundle would still run against the stale
486
+ // source so newly-needed deps stay un-mounted. latestSource is
487
+ // updated synchronously by updateSource alongside sourceVersion.
488
+ const currentSource = latestSource.current;
489
+ const currentVersion = sourceVersion.current;
490
+ const dependencyError = await installDependenciesForSource(
491
+ currentSource,
492
+ currentVersion,
493
+ );
494
+ if (executeEpoch.current !== epoch) return;
495
+ if (dependencyError) {
496
+ push("error", [dependencyError]);
497
+ return;
498
+ }
499
+ const service = await createCompilerService();
500
+ const compiled = await service.bundle({
501
+ source: currentSource,
502
+ options,
503
+ });
504
+ if (executeEpoch.current !== epoch) return;
505
+ if (compiled.type === "error") {
506
+ const message =
507
+ typeof compiled.value === "string"
508
+ ? compiled.value
509
+ : ((compiled.value as { message?: string })?.message ??
510
+ "Bundle failed");
511
+ setBundleError(message);
512
+ push("error", [compiled.value]);
513
+ return;
514
+ }
515
+ const code = compiled.value as string;
516
+ const sandboxConsole = {
517
+ log: (...args: unknown[]) => push("log", args),
518
+ info: (...args: unknown[]) => push("info", args),
519
+ warn: (...args: unknown[]) => push("warn", args),
520
+ error: (...args: unknown[]) => push("error", args),
521
+ debug: (...args: unknown[]) => push("debug", args),
522
+ dir: (...args: unknown[]) => push("dir", args),
523
+ table: (...args: unknown[]) => push("table", args),
524
+ };
525
+ try {
526
+ await executeBundle(code, {
527
+ console: sandboxConsole,
528
+ runtimeFiles: runtimeDependencyFiles.current,
529
+ });
530
+ } catch (error) {
531
+ push("error", [error]);
532
+ }
533
+ } catch (error) {
534
+ if (executeEpoch.current !== epoch) return;
535
+ push("error", [error]);
536
+ } finally {
537
+ // Only the winning epoch clears the flag — same rationale as
538
+ // compileEpoch above. With a separate executeEpoch this is robust
539
+ // against concurrent click + edit interleavings.
540
+ if (executeEpoch.current === epoch) setExecuting(false);
541
+ }
542
+ // `source` is intentionally NOT a dep: the body snapshots
543
+ // `latestSource.current` (always fresh ref) rather than reading the
544
+ // React state, so including `source` here would re-create the
545
+ // callback per keystroke and propagate into the global keydown
546
+ // useEffect, churning event-listener add/remove every character.
547
+ }, [createCompilerService, executeBundle, installDependenciesForSource, options]);
548
+
549
+ const allDiagnostics = useMemo(() => {
550
+ const fromCompile: ICompilerService.IDiagnostic[] = [];
551
+ if (result?.type === "failure") {
552
+ fromCompile.push(...result.diagnostics);
553
+ } else if (result?.type === "error") {
554
+ // Host-level exceptions (worker transport blip, wasm rejection)
555
+ // surface as a synthetic diagnostic so the diagnostics strip
556
+ // doesn't say "0 errors" while the result pane shows an error.
557
+ const message =
558
+ typeof result.value === "string"
559
+ ? result.value
560
+ : (((result.value as { message?: string })?.message ??
561
+ "ttsc: unexpected error") as string);
562
+ fromCompile.push({
563
+ line: 1,
564
+ column: 1,
565
+ length: 1,
566
+ severity: "error",
567
+ message,
568
+ code: "TTSC_RUNTIME",
569
+ });
570
+ }
571
+ const set = new Set<string>();
572
+ return [...fromCompile, ...lintDiagnostics].filter((d) => {
573
+ const key = `${d.line}:${d.column}:${d.code ?? ""}:${d.message}`;
574
+ if (set.has(key)) return false;
575
+ set.add(key);
576
+ return true;
577
+ });
578
+ }, [result, lintDiagnostics]);
579
+
580
+ useEffect(() => {
581
+ const onKey = (e: KeyboardEvent) => {
582
+ const mod = e.metaKey || e.ctrlKey;
583
+ if (!mod) return;
584
+ if (e.key === "Enter" && executeBundle) {
585
+ e.preventDefault();
586
+ void onExecute();
587
+ } else if (e.key.toLowerCase() === "s") {
588
+ e.preventDefault();
589
+ onShare();
590
+ } else if (e.key.toLowerCase() === "k") {
591
+ e.preventDefault();
592
+ document
593
+ .querySelector<HTMLButtonElement>(
594
+ "button[data-playground-examples-toggle]",
595
+ )
596
+ ?.click();
597
+ }
598
+ };
599
+ window.addEventListener("keydown", onKey);
600
+ return () => window.removeEventListener("keydown", onKey);
601
+ }, [executeBundle, onExecute, onShare]);
602
+
603
+ if (bootPhase === "failed") {
604
+ return (
605
+ <div className="flex flex-col h-screen w-full items-center justify-center bg-neutral-950 text-neutral-200 gap-5 px-6 text-center">
606
+ <span className="text-red-400 text-3xl">⚠</span>
607
+ <h1 className="text-lg font-mono">Playground failed to boot.</h1>
608
+ <pre className="max-w-xl text-[12px] font-mono text-neutral-400 whitespace-pre-wrap break-words">
609
+ {(() => {
610
+ const e = bootError;
611
+ if (e instanceof Error) return `${e.name}: ${e.message}`;
612
+ try {
613
+ return JSON.stringify(e, null, 2);
614
+ } catch {
615
+ return String(e);
616
+ }
617
+ })()}
618
+ </pre>
619
+ <button
620
+ onClick={() => {
621
+ // Each click bumps retryEpoch; only the latest retry's body
622
+ // writes boot state. A double-click cancels the prior retry's
623
+ // pending writes so it can't flip bootPhase to "ready"
624
+ // against a connection the new retry has already torn down.
625
+ const epoch = ++retryEpoch.current;
626
+ void (async () => {
627
+ await client.reset();
628
+ if (retryEpoch.current !== epoch) return;
629
+ setBootError(null);
630
+ setBootPhase("booting");
631
+ try {
632
+ await createCompilerService();
633
+ if (retryEpoch.current !== epoch) return;
634
+ setBootPhase("ready");
635
+ } catch (err) {
636
+ if (retryEpoch.current !== epoch) return;
637
+ setBootError(err);
638
+ setBootPhase("failed");
639
+ }
640
+ })();
641
+ }}
642
+ className="px-5 py-2 text-xs font-mono text-neutral-900 bg-white rounded-md hover:shadow-[0_0_30px_rgba(255,255,255,0.2)] transition-shadow"
643
+ >
644
+ Retry
645
+ </button>
646
+ </div>
647
+ );
648
+ }
649
+
650
+ return (
651
+ <div className="flex flex-col h-screen w-full bg-neutral-950 text-neutral-200 overflow-hidden">
652
+ {/* ── Toolbar ── */}
653
+ <div className="flex items-center gap-3 px-4 py-2.5 border-b border-neutral-800/70 bg-neutral-950 shrink-0">
654
+ {brand}
655
+ <span className="text-neutral-700">/</span>
656
+ <span className="text-sm text-neutral-400">Playground</span>
657
+
658
+ <div className="ml-auto flex items-center gap-2">
659
+ <ExamplePicker
660
+ examples={examples}
661
+ onPick={onPickExample}
662
+ groupLabels={exampleGroupLabels}
663
+ />
664
+ <button
665
+ onClick={() => setOptionsOpen((v) => !v)}
666
+ className="px-3 py-1.5 text-xs font-mono text-neutral-300 border border-neutral-800 rounded-md hover:border-neutral-600 hover:bg-neutral-900 transition-colors"
667
+ >
668
+ Options
669
+ </button>
670
+ <button
671
+ onClick={onReset}
672
+ className="px-3 py-1.5 text-xs font-mono text-neutral-400 hover:text-neutral-200 transition-colors"
673
+ >
674
+ Reset
675
+ </button>
676
+ <button
677
+ onClick={onShare}
678
+ className="px-3 py-1.5 text-xs font-mono text-neutral-900 bg-white rounded-md hover:shadow-[0_0_30px_rgba(255,255,255,0.2)] transition-shadow"
679
+ >
680
+ {shareToast ? "Copied ✓" : "Share"}
681
+ </button>
682
+ </div>
683
+ </div>
684
+
685
+ {sourceFromURL && (
686
+ <div className="shrink-0 px-4 py-1.5 text-[11px] font-mono text-amber-200 bg-amber-500/10 border-b border-amber-700/40">
687
+ Source loaded from share URL. Hit Reset to return to the default
688
+ example.
689
+ </div>
690
+ )}
691
+
692
+ {shareWarn && (
693
+ <div className="shrink-0 px-4 py-1.5 text-[11px] font-mono text-amber-200 bg-amber-500/10 border-b border-amber-700/40">
694
+ {shareWarn}
695
+ </div>
696
+ )}
697
+
698
+ <div className="flex items-center gap-0 border-b border-neutral-800/70 bg-neutral-950 shrink-0">
699
+ {(
700
+ [
701
+ { id: "javascript", label: "Compiled JS" },
702
+ { id: "lint", label: "Lint" },
703
+ ] as { id: Tab; label: string }[]
704
+ ).map((tab) => (
705
+ <button
706
+ key={tab.id}
707
+ onClick={() => setTarget(tab.id)}
708
+ className={`px-4 py-2 text-[12px] font-mono border-b-2 transition-colors ${
709
+ target === tab.id
710
+ ? "text-white border-blue-400"
711
+ : "text-neutral-500 border-transparent hover:text-neutral-300"
712
+ }`}
713
+ >
714
+ {tab.label}
715
+ </button>
716
+ ))}
717
+ <div className="ml-auto px-4 text-[10px] font-mono text-neutral-600">
718
+ {bootPhase === "booting"
719
+ ? "booting wasm…"
720
+ : running
721
+ ? "compiling…"
722
+ : "ready"}
723
+ </div>
724
+ </div>
725
+
726
+ <div className="flex flex-1 min-h-0 flex-col md:flex-row">
727
+ <div className="flex-1 min-w-0 md:border-r border-neutral-800/70 flex flex-col h-1/2 md:h-full">
728
+ <div className="flex items-center justify-between px-4 py-1.5 border-b border-neutral-800/70 bg-neutral-950">
729
+ <span className="text-[11px] font-mono text-neutral-500">
730
+ src/playground.ts
731
+ </span>
732
+ <span className="text-[10px] font-mono text-neutral-700">
733
+ {source.split("\n").length} lines
734
+ </span>
735
+ </div>
736
+ <div className="flex-1 min-h-0">
737
+ <SourceEditor
738
+ value={source}
739
+ onChange={updateSource}
740
+ extraLibs={mergedExtraLibs}
741
+ />
742
+ </div>
743
+ </div>
744
+
745
+ <div className="flex-1 min-w-0 flex flex-col h-1/2 md:h-full border-t md:border-t-0 border-neutral-800/70">
746
+ <div className="flex items-center justify-between px-4 py-1.5 border-b border-neutral-800/70 bg-neutral-950">
747
+ <span className="text-[11px] font-mono text-neutral-500">
748
+ {target === "javascript"
749
+ ? resultCaption(options)
750
+ : "lint diagnostics"}
751
+ </span>
752
+ <span className="text-[10px] font-mono text-neutral-700">
753
+ {result?.type === "error" ? "error" : ""}
754
+ </span>
755
+ </div>
756
+ <div className="flex-1 min-h-0">
757
+ {target === "lint" ? (
758
+ <LintPane diagnostics={lintDiagnostics} />
759
+ ) : (
760
+ <ResultViewer
761
+ language={result?.type === "error" ? "json" : "javascript"}
762
+ value={
763
+ result === null
764
+ ? ""
765
+ : result.type === "error"
766
+ ? (JSON.stringify(result.value, null, 2) ??
767
+ String(result.value))
768
+ : result.value
769
+ }
770
+ />
771
+ )}
772
+ </div>
773
+ </div>
774
+ </div>
775
+
776
+ {bundleError && (
777
+ <div className="shrink-0 px-4 py-1.5 text-[11px] font-mono text-red-300 bg-red-500/10 border-t border-red-700/40">
778
+ Bundle failed — {bundleError}
779
+ </div>
780
+ )}
781
+
782
+ {executeBundle && (
783
+ <div className="shrink-0 border-t border-neutral-800/70 bg-neutral-950 flex flex-col h-48">
784
+ <div className="flex items-center justify-between px-4 py-1.5 border-b border-neutral-800/70">
785
+ <span className="text-[11px] font-mono text-neutral-500">
786
+ console output
787
+ </span>
788
+ <div className="flex items-center gap-2">
789
+ {consoleMessages.length > 0 && (
790
+ <button
791
+ onClick={() => setConsoleMessages([])}
792
+ className="px-2 py-1 text-[10px] font-mono text-neutral-500 hover:text-neutral-200 transition-colors"
793
+ >
794
+ Clear
795
+ </button>
796
+ )}
797
+ <button
798
+ onClick={onExecute}
799
+ disabled={executing}
800
+ className="px-3 py-1 text-[11px] font-mono text-neutral-900 bg-emerald-400 rounded-md hover:bg-emerald-300 transition-colors disabled:opacity-50"
801
+ title="Cmd/Ctrl+Enter"
802
+ >
803
+ ▶ {executing ? "Executing…" : "Execute"}
804
+ </button>
805
+ </div>
806
+ </div>
807
+ <div className="flex-1 min-h-0">
808
+ <ConsoleViewer messages={consoleMessages} />
809
+ </div>
810
+ </div>
811
+ )}
812
+
813
+ <DiagnosticsPanel diagnostics={allDiagnostics} />
814
+
815
+ {optionsOpen && (
816
+ <OptionsPanel
817
+ options={options}
818
+ onChange={setOptions}
819
+ onClose={() => setOptionsOpen(false)}
820
+ toggles={optionToggles}
821
+ />
822
+ )}
823
+
824
+ <DependencyProgressModal
825
+ progress={dependencyProgress}
826
+ packages={dependencyPackageNames}
827
+ />
828
+ </div>
829
+ );
830
+ }
831
+
832
+ function defaultResultCaption(_options: ITransformOptions): string {
833
+ return "dist/playground.js";
834
+ }
835
+
836
+ function describeUnknownError(error: unknown): string {
837
+ if (error instanceof Error) return error.message;
838
+ try {
839
+ return JSON.stringify(error);
840
+ } catch {
841
+ return String(error);
842
+ }
843
+ }
844
+
845
+ function normalizeClientError(error: unknown): unknown {
846
+ if (error instanceof Error) {
847
+ return { name: error.name, message: error.message, stack: error.stack };
848
+ }
849
+ return { name: "Error", message: describeUnknownError(error) };
850
+ }
851
+
852
+ function wait(ms: number): Promise<void> {
853
+ return new Promise((resolve) => window.setTimeout(resolve, ms));
854
+ }
855
+
856
+ function createAbortError(reason: string): Error {
857
+ const error = new Error(`Dependency install aborted: ${reason}.`);
858
+ error.name = "AbortError";
859
+ return error;
860
+ }
861
+
862
+ function isAbortError(error: unknown): boolean {
863
+ return error instanceof Error && error.name === "AbortError";
864
+ }