From 7e7bf5fe2ac03f20d8ee6850bc8da7e9b426a25e Mon Sep 17 00:00:00 2001 From: jolov Date: Sun, 12 Apr 2026 19:48:39 -0700 Subject: [PATCH 1/3] feat: add multi-file state support to playground Add files, selectedFile to PlaygroundState. Normalize single-file content into files Record. Update compile function to write all files to virtual FS. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/playground/src/react/playground.tsx | 20 ++++++- .../src/react/use-playground-state.ts | 57 ++++++++++++++++++- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/packages/playground/src/react/playground.tsx b/packages/playground/src/react/playground.tsx index 4598b46ddf2..ef025555f4c 100644 --- a/packages/playground/src/react/playground.tsx +++ b/packages/playground/src/react/playground.tsx @@ -175,12 +175,17 @@ export const Playground: FunctionComponent = (props) => { selectedViewer, viewerState, content, + files, + selectedFile, + isMultiFile, onSelectedEmitterChange, onCompilerOptionsChange, onSelectedSampleNameChange, onSelectedViewerChange, onViewerStateChange, onContentChange, + onFileContentChange, + onSelectedFileChange, } = state; // Sync Monaco model with state content @@ -209,7 +214,7 @@ export const Playground: FunctionComponent = (props) => { const currentContent = typespecModel.getValue(); const typespecCompiler = host.compiler; - const state = await compile(host, currentContent, selectedEmitter, compilerOptions); + const state = await compile(host, currentContent, selectedEmitter, compilerOptions, files); setCompilationState(state); if ("program" in state) { const markers: editor.IMarkerData[] = state.program.diagnostics.map((diag) => ({ @@ -426,12 +431,21 @@ async function compile( content: string, selectedEmitter: string, options: CompilerOptions, + files?: Record, ): Promise { - await host.writeFile("main.tsp", content); + // Write all input files to the virtual FS + if (files && Object.keys(files).length > 1) { + for (const [path, fileContent] of Object.entries(files)) { + await host.writeFile(path, fileContent); + } + } else { + await host.writeFile("main.tsp", content); + } await emptyOutputDir(host); + const entrypoint = files && Object.keys(files).length > 1 ? "main.tsp" : "main.tsp"; try { const typespecCompiler = host.compiler; - const program = await typespecCompiler.compile(host, resolveVirtualPath("main.tsp"), { + const program = await typespecCompiler.compile(host, resolveVirtualPath(entrypoint), { ...options, options: { ...options.options, diff --git a/packages/playground/src/react/use-playground-state.ts b/packages/playground/src/react/use-playground-state.ts index 1b3a6fd5c38..e7f16952c1e 100644 --- a/packages/playground/src/react/use-playground-state.ts +++ b/packages/playground/src/react/use-playground-state.ts @@ -14,8 +14,16 @@ export interface PlaygroundState { selectedViewer?: string; /** Internal state of viewers */ viewerState?: Record; - /** TypeSpec content */ + /** TypeSpec content (single-file mode) */ content?: string; + /** + * Multiple input files (multi-file mode). + * Keys are file paths relative to the project root (e.g., "main.tsp", "models/widget.tsp"). + * When set, takes precedence over `content`. + */ + files?: Record; + /** Currently selected file in the editor (multi-file mode) */ + selectedFile?: string; } export interface UsePlaygroundStateProps { @@ -51,6 +59,13 @@ export interface PlaygroundStateResult { viewerState: Record; content: string; + /** All input files (normalized: always populated, even in single-file mode) */ + files: Record; + /** Currently selected file path */ + selectedFile: string; + /** Whether the playground is in multi-file mode */ + isMultiFile: boolean; + // State setters onSelectedEmitterChange: (emitter: string) => void; onCompilerOptionsChange: (compilerOptions: CompilerOptions) => void; @@ -58,6 +73,10 @@ export interface PlaygroundStateResult { onSelectedViewerChange: (selectedViewer: string) => void; onViewerStateChange: (viewerState: Record) => void; onContentChange: (content: string) => void; + onFilesChange: (files: Record) => void; + onSelectedFileChange: (selectedFile: string) => void; + /** Update the content of a specific file */ + onFileContentChange: (path: string, content: string) => void; // Full state management playgroundState: PlaygroundState; @@ -136,6 +155,36 @@ export function usePlaygroundState({ [updateState], ); const onContentChange = useCallback((content: string) => updateState({ content }), [updateState]); + const onFilesChange = useCallback( + (files: Record) => updateState({ files }), + [updateState], + ); + const onSelectedFileChange = useCallback( + (selectedFile: string) => updateState({ selectedFile }), + [updateState], + ); + const onFileContentChange = useCallback( + (path: string, fileContent: string) => { + const currentFiles = playgroundState.files ?? { "main.tsp": playgroundState.content ?? "" }; + updateState({ files: { ...currentFiles, [path]: fileContent } }); + }, + [updateState, playgroundState.files, playgroundState.content], + ); + + // Normalize files: always provide a Record, even in single-file mode + const files = useMemo>(() => { + if (playgroundState.files && Object.keys(playgroundState.files).length > 0) { + return playgroundState.files; + } + return { "main.tsp": content }; + }, [playgroundState.files, content]); + + const isMultiFile = useMemo( + () => Object.keys(files).length > 1, + [files], + ); + + const selectedFile = playgroundState.selectedFile ?? Object.keys(files)[0] ?? "main.tsp"; // Track last processed sample to avoid re-processing const lastProcessedSample = useRef(""); @@ -166,6 +215,9 @@ export function usePlaygroundState({ selectedViewer, viewerState: playgroundState.viewerState ?? {}, content, + files, + selectedFile, + isMultiFile, // State setters onSelectedEmitterChange, @@ -174,6 +226,9 @@ export function usePlaygroundState({ onSelectedViewerChange, onViewerStateChange, onContentChange, + onFilesChange, + onSelectedFileChange, + onFileContentChange, // Full state management playgroundState, From 95d0c2cc26623f615c69a46aa6ffc2109a95aae5 Mon Sep 17 00:00:00 2001 From: jolov Date: Sun, 12 Apr 2026 20:03:18 -0700 Subject: [PATCH 2/3] feat(playground): add multi-file editor with file explorer - Add useMonacoModels hook for managing multiple Monaco editor models - Add file tree explorer panel that shows when multiple input files exist - Update EditorPanel to accept inputFiles, selectedInputFile props - Wire multi-file compile: write all files to BrowserHost, clear stale ones - Add normalized multi-file accessors to playground state Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../editor-panel/editor-panel.module.css | 8 + .../src/react/editor-panel/editor-panel.tsx | 21 + packages/playground/src/react/editor.tsx | 66 + packages/playground/src/react/playground.tsx | 40 +- pnpm-lock.yaml | 1167 ++++++++--------- 5 files changed, 672 insertions(+), 630 deletions(-) diff --git a/packages/playground/src/react/editor-panel/editor-panel.module.css b/packages/playground/src/react/editor-panel/editor-panel.module.css index c45aca723db..c118a5faa09 100644 --- a/packages/playground/src/react/editor-panel/editor-panel.module.css +++ b/packages/playground/src/react/editor-panel/editor-panel.module.css @@ -8,6 +8,14 @@ background-color: var(--colorNeutralBackground3); } +.file-tree-container { + width: 220px; + min-width: 150px; + border-right: 1px solid var(--colorNeutralStroke1); + overflow-y: auto; + background-color: var(--colorNeutralBackground2); +} + .panel-content { flex: 1; min-width: 0; diff --git a/packages/playground/src/react/editor-panel/editor-panel.tsx b/packages/playground/src/react/editor-panel/editor-panel.tsx index 11369bb894d..2cc216ec6f7 100644 --- a/packages/playground/src/react/editor-panel/editor-panel.tsx +++ b/packages/playground/src/react/editor-panel/editor-panel.tsx @@ -5,6 +5,7 @@ import { editor } from "monaco-editor"; import { useCallback, useState, type FunctionComponent, type ReactNode } from "react"; import type { BrowserHost } from "../../types.js"; import type { OnMountData } from "../editor.js"; +import { FileTreeExplorer } from "../file-tree/file-tree.js"; import type { PlaygroundEditorsOptions } from "../playground.js"; import { TypeSpecEditor } from "../typespec-editor.js"; import { ConfigPanel } from "./config-panel.js"; @@ -42,6 +43,13 @@ export interface EditorPanelProps { /** Toolbar content rendered above the editor area */ commandBar?: ReactNode; + + /** List of input file paths for multi-file mode */ + inputFiles?: string[]; + /** Currently selected input file */ + selectedInputFile?: string; + /** Callback when a different input file is selected */ + onSelectedInputFileChange?: (file: string) => void; } export const EditorPanel: FunctionComponent = ({ @@ -55,8 +63,12 @@ export const EditorPanel: FunctionComponent = ({ onCompilerOptionsChange, onSelectedEmitterChange, commandBar, + inputFiles, + selectedInputFile, + onSelectedInputFileChange, }) => { const [selectedTab, setSelectedTab] = useState("tsp"); + const showFileTree = inputFiles && inputFiles.length > 1; const onTabSelect = useCallback((_, data) => { setSelectedTab(data.value as EditorPanelTab); @@ -78,6 +90,15 @@ export const EditorPanel: FunctionComponent = ({ + {showFileTree && selectedTab === "tsp" && ( +
+ onSelectedInputFileChange?.(file)} + /> +
+ )}
{commandBar} {selectedTab === "tsp" ? ( diff --git a/packages/playground/src/react/editor.tsx b/packages/playground/src/react/editor.tsx index 7c73486f92e..4255b792019 100644 --- a/packages/playground/src/react/editor.tsx +++ b/packages/playground/src/react/editor.tsx @@ -65,3 +65,69 @@ export function useMonacoModel(uri: string, language?: string): editor.IModel { return editor.getModel(monacoUri) ?? editor.createModel("", language, monacoUri); }, [uri, language]); } + +/** + * Manages multiple Monaco models for a set of files. + * Creates/updates/disposes models as files change. + * Returns the active model based on the selected file. + */ +export function useMonacoModels( + files: Record, + selectedFile: string, + language: string = "typespec", +): { activeModel: editor.IModel; allModels: Map } { + const modelsRef = useRef>(new Map()); + + // Sync models with files + const allModels = useMemo(() => { + const models = modelsRef.current; + const currentPaths = new Set(Object.keys(files)); + + // Remove models for deleted files + for (const [path, model] of models) { + if (!currentPaths.has(path)) { + model.dispose(); + models.delete(path); + } + } + + // Create or update models for current files + for (const [path, content] of Object.entries(files)) { + const uri = Uri.parse(`inmemory://test/${path}`); + let model = models.get(path); + if (!model) { + model = editor.getModel(uri) ?? editor.createModel(content, language, uri); + models.set(path, model); + } else if (model.getValue() !== content) { + model.setValue(content); + } + } + + return models; + }, [files, language]); + + // Get or create the active model + const activeModel = useMemo(() => { + const model = allModels.get(selectedFile); + if (model) return model; + + // Fallback: create a model for the selected file + const uri = Uri.parse(`inmemory://test/${selectedFile}`); + const fallback = + editor.getModel(uri) ?? editor.createModel(files[selectedFile] ?? "", language, uri); + allModels.set(selectedFile, fallback); + return fallback; + }, [allModels, selectedFile, files, language]); + + // Cleanup on unmount + useEffect(() => { + return () => { + for (const model of modelsRef.current.values()) { + model.dispose(); + } + modelsRef.current.clear(); + }; + }, []); + + return { activeModel, allModels }; +} diff --git a/packages/playground/src/react/playground.tsx b/packages/playground/src/react/playground.tsx index ef025555f4c..2986b7c6e1f 100644 --- a/packages/playground/src/react/playground.tsx +++ b/packages/playground/src/react/playground.tsx @@ -22,7 +22,7 @@ import { PlaygroundContextProvider } from "./context/playground-context.js"; import { debugGlobals, printDebugInfo } from "./debug.js"; import { DefaultFooter } from "./default-footer.js"; import { EditorPanel } from "./editor-panel/editor-panel.js"; -import { useMonacoModel, type OnMountData } from "./editor.js"; +import { useMonacoModels, type OnMountData } from "./editor.js"; import { OutputView } from "./output-view/output-view.js"; import style from "./playground.module.css"; import { ProblemPane } from "./problem-pane/index.js"; @@ -152,7 +152,6 @@ export const Playground: FunctionComponent = (props) => { debugGlobals().host = host; }, [host]); - const typespecModel = useMonacoModel("inmemory://test/main.tsp", "typespec"); const [compilationState, setCompilationState] = useState(undefined); // Use the playground state hook @@ -188,23 +187,25 @@ export const Playground: FunctionComponent = (props) => { onSelectedFileChange, } = state; - // Sync Monaco model with state content - useEffect(() => { - if (typespecModel.getValue() !== (content ?? "")) { - typespecModel.setValue(content ?? ""); - } - }, [content, typespecModel]); + // Manage Monaco models for all input files + const { activeModel: typespecModel } = useMonacoModels(files, selectedFile); // Update state when Monaco model changes useEffect(() => { const disposable = typespecModel.onDidChangeContent(() => { const newContent = typespecModel.getValue(); - if (newContent !== content) { - onContentChange(newContent); + if (isMultiFile) { + if (newContent !== files[selectedFile]) { + onFileContentChange(selectedFile, newContent); + } + } else { + if (newContent !== content) { + onContentChange(newContent); + } } }); return () => disposable.dispose(); - }, [typespecModel, content, onContentChange]); + }, [typespecModel, content, files, selectedFile, isMultiFile, onContentChange, onFileContentChange]); const isSampleUntouched = useMemo(() => { return Boolean(selectedSampleName && content === props.samples?.[selectedSampleName]?.content); @@ -236,7 +237,7 @@ export const Playground: FunctionComponent = (props) => { updateDiagnosticsForCodeFixes(typespecCompiler, []); editor.setModelMarkers(typespecModel, "owner", []); } - }, [host, selectedEmitter, compilerOptions, typespecModel]); + }, [host, selectedEmitter, compilerOptions, typespecModel, files]); useEffect(() => { const debouncer = debounce(() => doCompile(), 200); @@ -369,6 +370,9 @@ export const Playground: FunctionComponent = (props) => { onCompilerOptionsChange={onCompilerOptionsChange} onSelectedEmitterChange={onSelectedEmitterChange} commandBar={isMobile ? undefined : commandBar} + inputFiles={isMultiFile ? Object.keys(files) : undefined} + selectedInputFile={selectedFile} + onSelectedInputFileChange={onSelectedFileChange} /> ); @@ -433,6 +437,14 @@ async function compile( options: CompilerOptions, files?: Record, ): Promise { + // Clear previous source .tsp files from the virtual FS to avoid ghost imports + const existingFiles = await host.readDir("."); + for (const file of existingFiles) { + if (file.endsWith(".tsp")) { + await host.rm(file); + } + } + // Write all input files to the virtual FS if (files && Object.keys(files).length > 1) { for (const [path, fileContent] of Object.entries(files)) { @@ -441,11 +453,11 @@ async function compile( } else { await host.writeFile("main.tsp", content); } + await emptyOutputDir(host); - const entrypoint = files && Object.keys(files).length > 1 ? "main.tsp" : "main.tsp"; try { const typespecCompiler = host.compiler; - const program = await typespecCompiler.compile(host, resolveVirtualPath(entrypoint), { + const program = await typespecCompiler.compile(host, resolveVirtualPath("main.tsp"), { ...options, options: { ...options.options, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dcdceae8325..99aae7a2b16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,528 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -catalogs: - default: - '@alloy-js/cli': - specifier: ^0.22.0 - version: 0.22.0 - '@alloy-js/core': - specifier: ^0.22.0 - version: 0.22.0 - '@alloy-js/csharp': - specifier: ^0.22.0 - version: 0.22.0 - '@alloy-js/markdown': - specifier: ^0.22.0 - version: 0.22.0 - '@alloy-js/python': - specifier: ^0.3.0 - version: 0.3.0 - '@alloy-js/rollup-plugin': - specifier: ^0.1.0 - version: 0.1.0 - '@alloy-js/typescript': - specifier: ^0.22.0 - version: 0.22.0 - '@astrojs/check': - specifier: ^0.9.8 - version: 0.9.8 - '@astrojs/react': - specifier: ^5.0.3 - version: 5.0.3 - '@astrojs/starlight': - specifier: ^0.38.3 - version: 0.38.3 - '@azure/identity': - specifier: ^4.13.1 - version: 4.13.1 - '@azure/storage-blob': - specifier: ^12.31.0 - version: 12.31.0 - '@babel/code-frame': - specifier: ^7.29.0 - version: 7.29.0 - '@babel/core': - specifier: ^7.29.0 - version: 7.29.0 - '@chronus/chronus': - specifier: ^1.3.1 - version: 1.3.1 - '@chronus/github': - specifier: ^1.0.6 - version: 1.0.6 - '@chronus/github-pr-commenter': - specifier: ^1.0.6 - version: 1.0.6 - '@docsearch/css': - specifier: ^4.6.2 - version: 4.6.2 - '@docsearch/js': - specifier: ^4.6.2 - version: 4.6.2 - '@eslint/js': - specifier: ^10.0.1 - version: 10.0.1 - '@expressive-code/core': - specifier: ^0.41.7 - version: 0.41.7 - '@fluentui/react-components': - specifier: ^9.73.7 - version: 9.73.7 - '@fluentui/react-icons': - specifier: ^2.0.323 - version: 2.0.323 - '@fluentui/react-list': - specifier: ^9.6.13 - version: 9.6.13 - '@inquirer/prompts': - specifier: ^8.4.1 - version: 8.4.1 - '@microsoft/api-extractor': - specifier: ^7.58.1 - version: 7.58.1 - '@microsoft/api-extractor-model': - specifier: ^7.33.5 - version: 7.33.5 - '@microsoft/tsdoc': - specifier: ^0.16.0 - version: 0.16.0 - '@microsoft/tsdoc-config': - specifier: ^0.18.1 - version: 0.18.1 - '@octokit/core': - specifier: ^7.0.6 - version: 7.0.6 - '@octokit/plugin-paginate-graphql': - specifier: ^6.0.0 - version: 6.0.0 - '@octokit/plugin-rest-endpoint-methods': - specifier: ^17.0.0 - version: 17.0.0 - '@playwright/test': - specifier: ^1.59.1 - version: 1.59.1 - '@pnpm/workspace.find-packages': - specifier: ^1000.0.65 - version: 1000.0.65 - '@scalar/json-magic': - specifier: ^0.12.5 - version: 0.12.5 - '@scalar/openapi-parser': - specifier: ^0.25.8 - version: 0.25.8 - '@scalar/openapi-types': - specifier: ^0.7.0 - version: 0.7.0 - '@storybook/cli': - specifier: ^10.3.5 - version: 10.3.5 - '@storybook/react-vite': - specifier: ^10.3.5 - version: 10.3.5 - '@testing-library/dom': - specifier: ^10.4.1 - version: 10.4.1 - '@testing-library/jest-dom': - specifier: ^6.9.1 - version: 6.9.1 - '@testing-library/react': - specifier: ^16.3.2 - version: 16.3.2 - '@types/babel__code-frame': - specifier: ^7.27.0 - version: 7.27.0 - '@types/cross-spawn': - specifier: ^6.0.6 - version: 6.0.6 - '@types/debounce': - specifier: ^1.2.4 - version: 1.2.4 - '@types/express': - specifier: ^5.0.6 - version: 5.0.6 - '@types/micromatch': - specifier: ^4.0.10 - version: 4.0.10 - '@types/morgan': - specifier: ^1.9.10 - version: 1.9.10 - '@types/multer': - specifier: ^2.1.0 - version: 2.1.0 - '@types/mustache': - specifier: ^4.2.6 - version: 4.2.6 - '@types/node': - specifier: ^25.5.2 - version: 25.5.2 - '@types/plist': - specifier: ^3.0.5 - version: 3.0.5 - '@types/react': - specifier: ^19.2.14 - version: 19.2.14 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3 - '@types/remark-heading-id': - specifier: ^1.0.0 - version: 1.0.0 - '@types/semver': - specifier: ^7.7.1 - version: 7.7.1 - '@types/swagger-ui': - specifier: ^5.32.0 - version: 5.32.0 - '@types/swagger-ui-dist': - specifier: ^3.30.6 - version: 3.30.6 - '@types/swagger-ui-express': - specifier: ^4.1.8 - version: 4.1.8 - '@types/which': - specifier: ^3.0.4 - version: 3.0.4 - '@types/yargs': - specifier: ^17.0.35 - version: 17.0.35 - '@typescript-eslint/parser': - specifier: ^8.58.1 - version: 8.58.1 - '@typescript-eslint/rule-tester': - specifier: ^8.58.1 - version: 8.58.1 - '@typescript-eslint/types': - specifier: ^8.58.1 - version: 8.58.1 - '@typescript-eslint/utils': - specifier: ^8.58.1 - version: 8.58.1 - '@typespec/ts-http-runtime': - specifier: 0.3.4 - version: 0.3.4 - '@vitejs/plugin-react': - specifier: ^6.0.1 - version: 6.0.1 - '@vitest/coverage-v8': - specifier: ^4.1.3 - version: 4.1.3 - '@vitest/eslint-plugin': - specifier: ^1.6.14 - version: 1.6.14 - '@vitest/ui': - specifier: ^4.1.3 - version: 4.1.3 - '@vscode/extension-telemetry': - specifier: ^1.5.1 - version: 1.5.1 - '@vscode/test-electron': - specifier: ^2.5.2 - version: 2.5.2 - '@vscode/test-web': - specifier: ^0.0.80 - version: 0.0.80 - '@vscode/vsce': - specifier: ^3.7.1 - version: 3.7.1 - '@yarnpkg/core': - specifier: ^4.6.0 - version: 4.6.0 - '@yarnpkg/fslib': - specifier: ^3.1.4 - version: 3.1.5 - '@yarnpkg/plugin-nm': - specifier: ^4.0.8 - version: 4.0.8 - '@yarnpkg/plugin-npm': - specifier: ^3.4.0 - version: 3.4.1 - '@yarnpkg/plugin-pnp': - specifier: ^4.1.3 - version: 4.1.3 - ajv: - specifier: ^8.18.0 - version: 8.18.0 - ajv-formats: - specifier: ^3.0.1 - version: 3.0.1 - astro: - specifier: ^6.1.5 - version: 6.1.5 - astro-expressive-code: - specifier: ^0.41.7 - version: 0.41.7 - astro-rehype-relative-markdown-links: - specifier: ^0.19.0 - version: 0.19.0 - c8: - specifier: ^11.0.0 - version: 11.0.0 - change-case: - specifier: ^5.4.4 - version: 5.4.4 - chokidar: - specifier: ^5.0.0 - version: 5.0.0 - clsx: - specifier: ^2.1.1 - version: 2.1.1 - concurrently: - specifier: ^9.2.1 - version: 9.2.1 - cross-env: - specifier: ^10.1.0 - version: 10.1.0 - cross-spawn: - specifier: ^7.0.6 - version: 7.0.6 - cspell: - specifier: ^10.0.0 - version: 10.0.0 - debounce: - specifier: ^3.0.0 - version: 3.0.0 - decimal.js: - specifier: ^10.6.0 - version: 10.6.0 - ecmarkup: - specifier: ~23.0.2 - version: 23.0.2 - env-paths: - specifier: ^4.0.0 - version: 4.0.0 - es-module-shims: - specifier: ^2.8.0 - version: 2.8.0 - esbuild: - specifier: ^0.28.0 - version: 0.28.0 - esbuild-plugins-node-modules-polyfill: - specifier: ^1.8.1 - version: 1.8.1 - eslint: - specifier: ^10.2.0 - version: 10.2.0 - eslint-plugin-react-hooks: - specifier: 7.0.1 - version: 7.0.1 - eslint-plugin-unicorn: - specifier: ^64.0.0 - version: 64.0.0 - execa: - specifier: ^9.6.1 - version: 9.6.1 - express: - specifier: ^5.2.1 - version: 5.2.1 - fast-xml-parser: - specifier: ^5.5.9 - version: 5.5.10 - globby: - specifier: ^16.2.0 - version: 16.2.0 - happy-dom: - specifier: ^20.8.9 - version: 20.8.9 - is-unicode-supported: - specifier: ^2.1.0 - version: 2.1.0 - log-symbols: - specifier: ^7.0.1 - version: 7.0.1 - lzutf8: - specifier: 0.6.3 - version: 0.6.3 - micromatch: - specifier: ^4.0.8 - version: 4.0.8 - monaco-editor: - specifier: ^0.55.1 - version: 0.55.1 - monaco-editor-core: - specifier: ^0.55.1 - version: 0.55.1 - morgan: - specifier: ^1.10.1 - version: 1.10.1 - multer: - specifier: ^2.1.1 - version: 2.1.1 - mustache: - specifier: ^4.2.0 - version: 4.2.0 - ora: - specifier: ^9.3.0 - version: 9.3.0 - p-limit: - specifier: ^7.3.0 - version: 7.3.0 - pathe: - specifier: ^2.0.3 - version: 2.0.3 - picocolors: - specifier: ^1.1.1 - version: 1.1.1 - playwright: - specifier: ^1.59.1 - version: 1.59.1 - plist: - specifier: ^3.1.0 - version: 3.1.0 - postject: - specifier: 1.0.0-alpha.6 - version: 1.0.0-alpha.6 - prettier: - specifier: ^3.8.1 - version: 3.8.1 - prettier-plugin-astro: - specifier: ^0.14.1 - version: 0.14.1 - prettier-plugin-organize-imports: - specifier: ^4.3.0 - version: 4.3.0 - prettier-plugin-sh: - specifier: ^0.18.1 - version: 0.18.1 - prism-react-renderer: - specifier: ^2.4.1 - version: 2.4.1 - react: - specifier: ^19.2.4 - version: 19.2.4 - react-dom: - specifier: ^19.2.4 - version: 19.2.4 - react-error-boundary: - specifier: ^6.1.1 - version: 6.1.1 - react-hotkeys-hook: - specifier: ^5.2.4 - version: 5.2.4 - react-markdown: - specifier: ^10.1.0 - version: 10.1.0 - rehype-mermaid: - specifier: ^3.0.0 - version: 3.0.0 - remark-heading-id: - specifier: ^1.0.1 - version: 1.0.1 - rimraf: - specifier: ^6.1.3 - version: 6.1.3 - rollup-plugin-visualizer: - specifier: 7.0.1 - version: 7.0.1 - semver: - specifier: ^7.7.4 - version: 7.7.4 - sharp: - specifier: ^0.34.5 - version: 0.34.5 - simple-git: - specifier: ^3.35.2 - version: 3.35.2 - source-map-support: - specifier: ^0.5.21 - version: 0.5.21 - storybook: - specifier: ^10.3.5 - version: 10.3.5 - strip-json-comments: - specifier: ^5.0.3 - version: 5.0.3 - swagger-ui-dist: - specifier: ^5.32.2 - version: 5.32.2 - swagger-ui-express: - specifier: ^5.0.1 - version: 5.0.1 - tar: - specifier: ^7.5.13 - version: 7.5.13 - temporal-polyfill: - specifier: ^0.3.2 - version: 0.3.2 - tree-sitter-c-sharp: - specifier: ^0.23.1 - version: 0.23.1 - tree-sitter-java: - specifier: ^0.23.5 - version: 0.23.5 - tree-sitter-javascript: - specifier: ^0.25.0 - version: 0.25.0 - tree-sitter-python: - specifier: ^0.25.0 - version: 0.25.0 - tree-sitter-typescript: - specifier: ^0.23.2 - version: 0.23.2 - tsx: - specifier: ^4.21.0 - version: 4.21.0 - typedoc: - specifier: ^0.28.18 - version: 0.28.18 - typedoc-plugin-markdown: - specifier: ^4.11.0 - version: 4.11.0 - typescript: - specifier: ~6.0.2 - version: 6.0.2 - typescript-eslint: - specifier: ^8.58.1 - version: 8.58.1 - uri-template: - specifier: ^2.0.0 - version: 2.0.0 - vite: - specifier: ^8.0.7 - version: 8.0.7 - vite-plugin-checker: - specifier: ^0.12.0 - version: 0.12.0 - vite-plugin-dts: - specifier: 4.5.4 - version: 4.5.4 - vitest: - specifier: ^4.1.3 - version: 4.1.3 - vscode-languageclient: - specifier: ^9.0.1 - version: 9.0.1 - vscode-languageserver: - specifier: ^9.0.1 - version: 9.0.1 - vscode-languageserver-textdocument: - specifier: ^1.0.12 - version: 1.0.12 - vscode-oniguruma: - specifier: ^2.0.1 - version: 2.0.1 - vscode-textmate: - specifier: ^9.3.2 - version: 9.3.2 - web-tree-sitter: - specifier: ^0.26.8 - version: 0.26.8 - which: - specifier: ^6.0.1 - version: 6.0.1 - yaml: - specifier: ^2.8.3 - version: 2.8.3 - yargs: - specifier: ^18.0.0 - version: 18.0.0 - -overrides: - cross-spawn@>=7.0.0 <7.0.5: '>=7.0.5' - diff@>=6.0.0 <8.0.3: '>=8.0.3' - dompurify: ^3.3.3 - yaml@>=2.0.0 <2.8.3: '>=2.8.3' - importers: .: @@ -565,7 +43,7 @@ importers: version: 4.1.3(vitest@4.1.3) '@vitest/eslint-plugin': specifier: 'catalog:' - version: 1.6.14(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2)(vitest@4.1.3) + version: 1.6.14(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2)(vitest@4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) c8: specifier: 'catalog:' version: 11.0.0 @@ -4865,105 +4343,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -6004,28 +5466,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@reflink/reflink-linux-arm64-musl@0.1.19': resolution: {integrity: sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@reflink/reflink-linux-x64-gnu@0.1.19': resolution: {integrity: sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@reflink/reflink-linux-x64-musl@0.1.19': resolution: {integrity: sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@reflink/reflink-win32-arm64-msvc@0.1.19': resolution: {integrity: sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ==} @@ -6082,42 +5540,36 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.13': resolution: {integrity: sha512-bmRg3O6Z0gq9yodKKWCIpnlH051sEfdVwt+6m5UDffAQMUUqU0xjnQqqAUm+Gu7ofAAly9DqiQDtKu2nPDEABA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.13': resolution: {integrity: sha512-8Wtnbw4k7pMYN9B/mOEAsQ8HOiq7AZ31Ig4M9BKn2So4xRaFEhtCSa4ZJaOutOWq50zpgR4N5+L/opnlaCx8wQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.13': resolution: {integrity: sha512-D/0Nlo8mQuxSMohNJUF2lDXWRsFDsHldfRRgD9bRgktj+EndGPj4DOV37LqDKPYS+osdyhZEH7fTakTAEcW7qg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.13': resolution: {integrity: sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.13': resolution: {integrity: sha512-PsdONiFRp8hR8KgVjTWjZ9s7uA3uueWL0t74/cKHfM4dR5zXYv4AjB8BvA+QDToqxAFg4ZkcVEqeu5F7inoz5w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.13': resolution: {integrity: sha512-hCNXgC5dI3TVOLrPT++PKFNZ+1EtS0mLQwfXXXSUD/+rGlB65gZDwN/IDuxLpQP4x8RYYHqGomlUXzpO8aVI2w==} @@ -6207,85 +5659,71 @@ packages: resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.1': resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.1': resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.1': resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.1': resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.1': resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.1': resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.1': resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.1': resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.1': resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.1': resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.53.3': resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.1': resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.1': resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.60.1': resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} @@ -8354,6 +7792,10 @@ packages: engines: {node: '>=20'} hasBin: true + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -8788,6 +8230,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.2.7: + resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} + dompurify@3.3.3: resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} @@ -10202,28 +9647,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -13101,7 +12542,7 @@ packages: sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 - yaml: '>=2.8.3' + yaml: ^2.4.2 peerDependenciesMeta: '@types/node': optional: true @@ -13142,7 +12583,7 @@ packages: sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 - yaml: '>=2.8.3' + yaml: ^2.4.2 peerDependenciesMeta: '@types/node': optional: true @@ -13524,6 +12965,11 @@ packages: resolution: {integrity: sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA==} hasBin: true + yaml@2.7.1: + resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} + engines: {node: '>= 14'} + hasBin: true + yaml@2.8.3: resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} @@ -13582,6 +13028,522 @@ packages: zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} +catalogs: + default: + '@alloy-js/cli': + specifier: ^0.22.0 + version: 0.22.0 + '@alloy-js/core': + specifier: ^0.22.0 + version: 0.22.0 + '@alloy-js/csharp': + specifier: ^0.22.0 + version: 0.22.0 + '@alloy-js/markdown': + specifier: ^0.22.0 + version: 0.22.0 + '@alloy-js/python': + specifier: ^0.3.0 + version: 0.3.0 + '@alloy-js/rollup-plugin': + specifier: ^0.1.0 + version: 0.1.0 + '@alloy-js/typescript': + specifier: ^0.22.0 + version: 0.22.0 + '@astrojs/check': + specifier: ^0.9.8 + version: 0.9.8 + '@astrojs/react': + specifier: ^5.0.3 + version: 5.0.3 + '@astrojs/starlight': + specifier: ^0.38.3 + version: 0.38.3 + '@azure/identity': + specifier: ^4.13.1 + version: 4.13.1 + '@azure/storage-blob': + specifier: ^12.31.0 + version: 12.31.0 + '@babel/code-frame': + specifier: ^7.29.0 + version: 7.29.0 + '@babel/core': + specifier: ^7.29.0 + version: 7.29.0 + '@chronus/chronus': + specifier: ^1.3.1 + version: 1.3.1 + '@chronus/github': + specifier: ^1.0.6 + version: 1.0.6 + '@chronus/github-pr-commenter': + specifier: ^1.0.6 + version: 1.0.6 + '@docsearch/css': + specifier: ^4.6.2 + version: 4.6.2 + '@docsearch/js': + specifier: ^4.6.2 + version: 4.6.2 + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1 + '@expressive-code/core': + specifier: ^0.41.7 + version: 0.41.7 + '@fluentui/react-components': + specifier: ^9.73.7 + version: 9.73.7 + '@fluentui/react-icons': + specifier: ^2.0.323 + version: 2.0.323 + '@fluentui/react-list': + specifier: ^9.6.13 + version: 9.6.13 + '@inquirer/prompts': + specifier: ^8.4.1 + version: 8.4.1 + '@microsoft/api-extractor': + specifier: ^7.58.1 + version: 7.58.1 + '@microsoft/api-extractor-model': + specifier: ^7.33.5 + version: 7.33.5 + '@microsoft/tsdoc': + specifier: ^0.16.0 + version: 0.16.0 + '@microsoft/tsdoc-config': + specifier: ^0.18.1 + version: 0.18.1 + '@octokit/core': + specifier: ^7.0.6 + version: 7.0.6 + '@octokit/plugin-paginate-graphql': + specifier: ^6.0.0 + version: 6.0.0 + '@octokit/plugin-rest-endpoint-methods': + specifier: ^17.0.0 + version: 17.0.0 + '@playwright/test': + specifier: ^1.59.1 + version: 1.59.1 + '@pnpm/workspace.find-packages': + specifier: ^1000.0.65 + version: 1000.0.65 + '@scalar/json-magic': + specifier: ^0.12.5 + version: 0.12.5 + '@scalar/openapi-parser': + specifier: ^0.25.8 + version: 0.25.8 + '@scalar/openapi-types': + specifier: ^0.7.0 + version: 0.7.0 + '@storybook/cli': + specifier: ^10.3.5 + version: 10.3.5 + '@storybook/react-vite': + specifier: ^10.3.5 + version: 10.3.5 + '@testing-library/dom': + specifier: ^10.4.1 + version: 10.4.1 + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2 + '@types/babel__code-frame': + specifier: ^7.27.0 + version: 7.27.0 + '@types/cross-spawn': + specifier: ^6.0.6 + version: 6.0.6 + '@types/debounce': + specifier: ^1.2.4 + version: 1.2.4 + '@types/express': + specifier: ^5.0.6 + version: 5.0.6 + '@types/micromatch': + specifier: ^4.0.10 + version: 4.0.10 + '@types/morgan': + specifier: ^1.9.10 + version: 1.9.10 + '@types/multer': + specifier: ^2.1.0 + version: 2.1.0 + '@types/mustache': + specifier: ^4.2.6 + version: 4.2.6 + '@types/node': + specifier: ^25.5.2 + version: 25.5.2 + '@types/plist': + specifier: ^3.0.5 + version: 3.0.5 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3 + '@types/remark-heading-id': + specifier: ^1.0.0 + version: 1.0.0 + '@types/semver': + specifier: ^7.7.1 + version: 7.7.1 + '@types/swagger-ui': + specifier: ^5.32.0 + version: 5.32.0 + '@types/swagger-ui-dist': + specifier: ^3.30.6 + version: 3.30.6 + '@types/swagger-ui-express': + specifier: ^4.1.8 + version: 4.1.8 + '@types/which': + specifier: ^3.0.4 + version: 3.0.4 + '@types/yargs': + specifier: ^17.0.35 + version: 17.0.35 + '@typescript-eslint/parser': + specifier: ^8.58.1 + version: 8.58.1 + '@typescript-eslint/rule-tester': + specifier: ^8.58.1 + version: 8.58.1 + '@typescript-eslint/types': + specifier: ^8.58.1 + version: 8.58.1 + '@typescript-eslint/utils': + specifier: ^8.58.1 + version: 8.58.1 + '@typespec/ts-http-runtime': + specifier: 0.3.4 + version: 0.3.4 + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1 + '@vitest/coverage-v8': + specifier: ^4.1.3 + version: 4.1.3 + '@vitest/eslint-plugin': + specifier: ^1.6.14 + version: 1.6.14 + '@vitest/ui': + specifier: ^4.1.3 + version: 4.1.3 + '@vscode/extension-telemetry': + specifier: ^1.5.1 + version: 1.5.1 + '@vscode/test-electron': + specifier: ^2.5.2 + version: 2.5.2 + '@vscode/test-web': + specifier: ^0.0.80 + version: 0.0.80 + '@vscode/vsce': + specifier: ^3.7.1 + version: 3.7.1 + '@yarnpkg/core': + specifier: ^4.6.0 + version: 4.6.0 + '@yarnpkg/fslib': + specifier: ^3.1.4 + version: 3.1.5 + '@yarnpkg/plugin-nm': + specifier: ^4.0.8 + version: 4.0.8 + '@yarnpkg/plugin-npm': + specifier: ^3.4.0 + version: 3.4.1 + '@yarnpkg/plugin-pnp': + specifier: ^4.1.3 + version: 4.1.3 + ajv: + specifier: ^8.18.0 + version: 8.18.0 + ajv-formats: + specifier: ^3.0.1 + version: 3.0.1 + astro: + specifier: ^6.1.5 + version: 6.1.5 + astro-expressive-code: + specifier: ^0.41.7 + version: 0.41.7 + astro-rehype-relative-markdown-links: + specifier: ^0.19.0 + version: 0.19.0 + c8: + specifier: ^11.0.0 + version: 11.0.0 + change-case: + specifier: ^5.4.4 + version: 5.4.4 + chokidar: + specifier: ^5.0.0 + version: 5.0.0 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + concurrently: + specifier: ^9.2.1 + version: 9.2.1 + cross-env: + specifier: ^10.1.0 + version: 10.1.0 + cross-spawn: + specifier: ^7.0.6 + version: 7.0.6 + cspell: + specifier: ^10.0.0 + version: 10.0.0 + debounce: + specifier: ^3.0.0 + version: 3.0.0 + decimal.js: + specifier: ^10.6.0 + version: 10.6.0 + ecmarkup: + specifier: ~23.0.2 + version: 23.0.2 + env-paths: + specifier: ^4.0.0 + version: 4.0.0 + es-module-shims: + specifier: ^2.8.0 + version: 2.8.0 + esbuild: + specifier: ^0.28.0 + version: 0.28.0 + esbuild-plugins-node-modules-polyfill: + specifier: ^1.8.1 + version: 1.8.1 + eslint: + specifier: ^10.2.0 + version: 10.2.0 + eslint-plugin-react-hooks: + specifier: 7.0.1 + version: 7.0.1 + eslint-plugin-unicorn: + specifier: ^64.0.0 + version: 64.0.0 + execa: + specifier: ^9.6.1 + version: 9.6.1 + express: + specifier: ^5.2.1 + version: 5.2.1 + fast-xml-parser: + specifier: ^5.5.9 + version: 5.5.10 + globby: + specifier: ^16.2.0 + version: 16.2.0 + happy-dom: + specifier: ^20.8.9 + version: 20.8.9 + is-unicode-supported: + specifier: ^2.1.0 + version: 2.1.0 + log-symbols: + specifier: ^7.0.1 + version: 7.0.1 + lzutf8: + specifier: 0.6.3 + version: 0.6.3 + micromatch: + specifier: ^4.0.8 + version: 4.0.8 + monaco-editor: + specifier: ^0.55.1 + version: 0.55.1 + monaco-editor-core: + specifier: ^0.55.1 + version: 0.55.1 + morgan: + specifier: ^1.10.1 + version: 1.10.1 + multer: + specifier: ^2.1.1 + version: 2.1.1 + mustache: + specifier: ^4.2.0 + version: 4.2.0 + ora: + specifier: ^9.3.0 + version: 9.3.0 + p-limit: + specifier: ^7.3.0 + version: 7.3.0 + pathe: + specifier: ^2.0.3 + version: 2.0.3 + picocolors: + specifier: ^1.1.1 + version: 1.1.1 + playwright: + specifier: ^1.59.1 + version: 1.59.1 + plist: + specifier: ^3.1.0 + version: 3.1.0 + postject: + specifier: 1.0.0-alpha.6 + version: 1.0.0-alpha.6 + prettier: + specifier: ^3.8.1 + version: 3.8.1 + prettier-plugin-astro: + specifier: ^0.14.1 + version: 0.14.1 + prettier-plugin-organize-imports: + specifier: ^4.3.0 + version: 4.3.0 + prettier-plugin-sh: + specifier: ^0.18.1 + version: 0.18.1 + prism-react-renderer: + specifier: ^2.4.1 + version: 2.4.1 + react: + specifier: ^19.2.4 + version: 19.2.4 + react-dom: + specifier: ^19.2.4 + version: 19.2.4 + react-error-boundary: + specifier: ^6.1.1 + version: 6.1.1 + react-hotkeys-hook: + specifier: ^5.2.4 + version: 5.2.4 + react-markdown: + specifier: ^10.1.0 + version: 10.1.0 + rehype-mermaid: + specifier: ^3.0.0 + version: 3.0.0 + remark-heading-id: + specifier: ^1.0.1 + version: 1.0.1 + rimraf: + specifier: ^6.1.3 + version: 6.1.3 + rollup-plugin-visualizer: + specifier: 7.0.1 + version: 7.0.1 + semver: + specifier: ^7.7.4 + version: 7.7.4 + sharp: + specifier: ^0.34.5 + version: 0.34.5 + simple-git: + specifier: ^3.35.2 + version: 3.35.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + storybook: + specifier: ^10.3.5 + version: 10.3.5 + strip-json-comments: + specifier: ^5.0.3 + version: 5.0.3 + swagger-ui-dist: + specifier: ^5.32.2 + version: 5.32.2 + swagger-ui-express: + specifier: ^5.0.1 + version: 5.0.1 + tar: + specifier: ^7.5.13 + version: 7.5.13 + temporal-polyfill: + specifier: ^0.3.2 + version: 0.3.2 + tree-sitter-c-sharp: + specifier: ^0.23.1 + version: 0.23.1 + tree-sitter-java: + specifier: ^0.23.5 + version: 0.23.5 + tree-sitter-javascript: + specifier: ^0.25.0 + version: 0.25.0 + tree-sitter-python: + specifier: ^0.25.0 + version: 0.25.0 + tree-sitter-typescript: + specifier: ^0.23.2 + version: 0.23.2 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typedoc: + specifier: ^0.28.18 + version: 0.28.18 + typedoc-plugin-markdown: + specifier: ^4.11.0 + version: 4.11.0 + typescript: + specifier: ~6.0.2 + version: 6.0.2 + typescript-eslint: + specifier: ^8.58.1 + version: 8.58.1 + uri-template: + specifier: ^2.0.0 + version: 2.0.0 + vite: + specifier: ^8.0.7 + version: 8.0.7 + vite-plugin-checker: + specifier: ^0.12.0 + version: 0.12.0 + vite-plugin-dts: + specifier: 4.5.4 + version: 4.5.4 + vitest: + specifier: ^4.1.3 + version: 4.1.3 + vscode-languageclient: + specifier: ^9.0.1 + version: 9.0.1 + vscode-languageserver: + specifier: ^9.0.1 + version: 9.0.1 + vscode-languageserver-textdocument: + specifier: ^1.0.12 + version: 1.0.12 + vscode-oniguruma: + specifier: ^2.0.1 + version: 2.0.1 + vscode-textmate: + specifier: ^9.3.2 + version: 9.3.2 + web-tree-sitter: + specifier: ^0.26.8 + version: 0.26.8 + which: + specifier: ^6.0.1 + version: 6.0.1 + yaml: + specifier: ^2.8.3 + version: 2.8.3 + yargs: + specifier: ^18.0.0 + version: 18.0.0 + snapshots: '@adobe/css-tools@4.4.4': {} @@ -19149,7 +19111,7 @@ snapshots: tinyrainbow: 3.1.0 vitest: 4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/eslint-plugin@1.6.14(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2)(vitest@4.1.3)': + '@vitest/eslint-plugin@1.6.14(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2)(vitest@4.1.3(@types/node@25.5.2)(@vitest/coverage-v8@4.1.3)(@vitest/ui@4.1.3)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.28.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@typescript-eslint/scope-manager': 8.58.1 '@typescript-eslint/utils': 8.58.1(eslint@10.2.0)(typescript@6.0.2) @@ -19526,7 +19488,7 @@ snapshots: '@yarnpkg/libui@3.1.0(ink@3.2.0(@types/react@19.2.14)(react@17.0.2))(react@17.0.2)': dependencies: - ink: 3.2.0(@types/react@19.2.14)(react@19.2.4) + ink: 3.2.0(@types/react@19.2.14)(react@17.0.2) react: 17.0.2 tslib: 2.8.1 @@ -19657,7 +19619,7 @@ snapshots: algoliasearch: 4.27.0 clipanion: 4.0.0-rc.4(typanion@3.14.0) diff: 5.2.2 - ink: 3.2.0(@types/react@19.2.14)(react@19.2.4) + ink: 3.2.0(@types/react@19.2.14)(react@17.0.2) ink-text-input: 4.0.3(ink@3.2.0(@types/react@19.2.14)(react@17.0.2))(react@17.0.2) react: 17.0.2 semver: 7.7.4 @@ -19841,7 +19803,7 @@ snapshots: '@yarnpkg/parsers': 3.0.3 chalk: 3.0.0 clipanion: 4.0.0-rc.4(typanion@3.14.0) - cross-spawn: 7.0.6 + cross-spawn: 7.0.3 fast-glob: 3.3.3 micromatch: 4.0.8 tslib: 2.8.1 @@ -20786,6 +20748,12 @@ snapshots: '@epic-web/invariant': 1.0.0 cross-spawn: 7.0.6 + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -21250,6 +21218,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.2.7: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dompurify@3.3.3: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -22541,7 +22513,7 @@ snapshots: ink-text-input@4.0.3(ink@3.2.0(@types/react@19.2.14)(react@17.0.2))(react@17.0.2): dependencies: chalk: 4.1.2 - ink: 3.2.0(@types/react@19.2.14)(react@19.2.4) + ink: 3.2.0(@types/react@19.2.14)(react@17.0.2) react: 17.0.2 type-fest: 0.15.1 @@ -22577,38 +22549,6 @@ snapshots: - bufferutil - utf-8-validate - ink@3.2.0(@types/react@19.2.14)(react@19.2.4): - dependencies: - ansi-escapes: 4.3.2 - auto-bind: 4.0.0 - chalk: 4.1.2 - cli-boxes: 2.2.1 - cli-cursor: 3.1.0 - cli-truncate: 2.1.0 - code-excerpt: 3.0.0 - indent-string: 4.0.0 - is-ci: 2.0.0 - lodash: 4.18.1 - patch-console: 1.0.0 - react: 19.2.4 - react-devtools-core: 4.28.5 - react-reconciler: 0.26.2(react@19.2.4) - scheduler: 0.20.2 - signal-exit: 3.0.7 - slice-ansi: 3.0.0 - stack-utils: 2.0.6 - string-width: 4.2.3 - type-fest: 0.12.0 - widest-line: 3.1.0 - wrap-ansi: 6.2.0 - ws: 7.5.10 - yoga-layout-prebuilt: 1.10.0 - optionalDependencies: - '@types/react': 19.2.14 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - inline-style-parser@0.2.7: {} internmap@1.0.1: {} @@ -23864,12 +23804,12 @@ snapshots: monaco-editor-core@0.55.1: dependencies: - dompurify: 3.3.3 + dompurify: 3.2.7 marked: 14.0.0 monaco-editor@0.55.1: dependencies: - dompurify: 3.3.3 + dompurify: 3.2.7 marked: 14.0.0 morgan@1.10.1: @@ -24780,13 +24720,6 @@ snapshots: react: 17.0.2 scheduler: 0.20.2 - react-reconciler@0.26.2(react@19.2.4): - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react: 19.2.4 - scheduler: 0.20.2 - react-refresh@0.18.0: {} react@17.0.2: @@ -26765,7 +26698,9 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.17.5 vscode-uri: 3.1.0 - yaml: 2.8.3 + yaml: 2.7.1 + + yaml@2.7.1: {} yaml@2.8.3: {} From 70b1bc5bf32a713ae179b5023e68acb7b85aba02 Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 13 Apr 2026 10:30:21 -0700 Subject: [PATCH 3/3] test: add multi-file state tests and changeset - 13 tests for usePlaygroundState multi-file support - Tests cover: files normalization, isMultiFile, selectedFile defaults, onSelectedFileChange, onFilesChange, onFileContentChange - Add changeset for @typespec/playground Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...yground-file-explorer-2026-3-13-10-30-8.md | 7 + packages/playground/src/react/playground.tsx | 10 +- .../src/react/use-playground-state.ts | 5 +- .../test/use-playground-state.test.ts | 120 ++++++++++++++++++ 4 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 .chronus/changes/feat-playground-file-explorer-2026-3-13-10-30-8.md create mode 100644 packages/playground/test/use-playground-state.test.ts diff --git a/.chronus/changes/feat-playground-file-explorer-2026-3-13-10-30-8.md b/.chronus/changes/feat-playground-file-explorer-2026-3-13-10-30-8.md new file mode 100644 index 00000000000..d3a4ee34bdb --- /dev/null +++ b/.chronus/changes/feat-playground-file-explorer-2026-3-13-10-30-8.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/playground" +--- + +Add multi-file editor with file explorer support \ No newline at end of file diff --git a/packages/playground/src/react/playground.tsx b/packages/playground/src/react/playground.tsx index 2986b7c6e1f..e595307f794 100644 --- a/packages/playground/src/react/playground.tsx +++ b/packages/playground/src/react/playground.tsx @@ -205,7 +205,15 @@ export const Playground: FunctionComponent = (props) => { } }); return () => disposable.dispose(); - }, [typespecModel, content, files, selectedFile, isMultiFile, onContentChange, onFileContentChange]); + }, [ + typespecModel, + content, + files, + selectedFile, + isMultiFile, + onContentChange, + onFileContentChange, + ]); const isSampleUntouched = useMemo(() => { return Boolean(selectedSampleName && content === props.samples?.[selectedSampleName]?.content); diff --git a/packages/playground/src/react/use-playground-state.ts b/packages/playground/src/react/use-playground-state.ts index e7f16952c1e..bfdda407198 100644 --- a/packages/playground/src/react/use-playground-state.ts +++ b/packages/playground/src/react/use-playground-state.ts @@ -179,10 +179,7 @@ export function usePlaygroundState({ return { "main.tsp": content }; }, [playgroundState.files, content]); - const isMultiFile = useMemo( - () => Object.keys(files).length > 1, - [files], - ); + const isMultiFile = useMemo(() => Object.keys(files).length > 1, [files]); const selectedFile = playgroundState.selectedFile ?? Object.keys(files)[0] ?? "main.tsp"; diff --git a/packages/playground/test/use-playground-state.test.ts b/packages/playground/test/use-playground-state.test.ts new file mode 100644 index 00000000000..06815bb9ecf --- /dev/null +++ b/packages/playground/test/use-playground-state.test.ts @@ -0,0 +1,120 @@ +import { act, renderHook } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { usePlaygroundState, type PlaygroundState } from "../src/react/use-playground-state.js"; + +function renderPlaygroundState(initialState: PlaygroundState = {}) { + return renderHook(() => + usePlaygroundState({ + libraries: ["@typespec/http"], + defaultPlaygroundState: { emitter: "@typespec/openapi3", ...initialState }, + }), + ); +} + +describe("usePlaygroundState multi-file support", () => { + describe("files normalization", () => { + it("normalizes single-file content to { 'main.tsp': content }", () => { + const { result } = renderPlaygroundState({ content: "model Foo {}" }); + expect(result.current.files).toEqual({ "main.tsp": "model Foo {}" }); + }); + + it("uses files when provided", () => { + const files = { "main.tsp": "import './models';", "models.tsp": "model Bar {}" }; + const { result } = renderPlaygroundState({ files }); + expect(result.current.files).toEqual(files); + }); + + it("defaults to main.tsp with empty content when nothing provided", () => { + const { result } = renderPlaygroundState({}); + expect(result.current.files).toEqual({ "main.tsp": "" }); + }); + }); + + describe("isMultiFile", () => { + it("returns false for single-file mode", () => { + const { result } = renderPlaygroundState({ content: "model Foo {}" }); + expect(result.current.isMultiFile).toBe(false); + }); + + it("returns true when multiple files are present", () => { + const files = { "main.tsp": "import './models';", "models.tsp": "model Bar {}" }; + const { result } = renderPlaygroundState({ files }); + expect(result.current.isMultiFile).toBe(true); + }); + + it("returns false when files has a single entry", () => { + const { result } = renderPlaygroundState({ files: { "main.tsp": "model Foo {}" } }); + expect(result.current.isMultiFile).toBe(false); + }); + }); + + describe("selectedFile", () => { + it("defaults to first file when not specified", () => { + const files = { "main.tsp": "", "models.tsp": "" }; + const { result } = renderPlaygroundState({ files }); + expect(result.current.selectedFile).toBe("main.tsp"); + }); + + it("uses selectedFile when specified", () => { + const files = { "main.tsp": "", "models.tsp": "" }; + const { result } = renderPlaygroundState({ files, selectedFile: "models.tsp" }); + expect(result.current.selectedFile).toBe("models.tsp"); + }); + + it("defaults to main.tsp in single-file mode", () => { + const { result } = renderPlaygroundState({ content: "model Foo {}" }); + expect(result.current.selectedFile).toBe("main.tsp"); + }); + }); + + describe("onSelectedFileChange", () => { + it("changes the selected file", () => { + const files = { "main.tsp": "", "models.tsp": "" }; + const { result } = renderPlaygroundState({ files }); + + act(() => { + result.current.onSelectedFileChange("models.tsp"); + }); + + expect(result.current.selectedFile).toBe("models.tsp"); + }); + }); + + describe("onFilesChange", () => { + it("updates all files", () => { + const { result } = renderPlaygroundState({ content: "model Foo {}" }); + const newFiles = { "main.tsp": "import './bar';", "bar.tsp": "model Bar {}" }; + + act(() => { + result.current.onFilesChange(newFiles); + }); + + expect(result.current.files).toEqual(newFiles); + expect(result.current.isMultiFile).toBe(true); + }); + }); + + describe("onFileContentChange", () => { + it("updates a specific file in multi-file mode", () => { + const files = { "main.tsp": "import './models';", "models.tsp": "model Bar {}" }; + const { result } = renderPlaygroundState({ files }); + + act(() => { + result.current.onFileContentChange("models.tsp", "model UpdatedBar {}"); + }); + + expect(result.current.files["models.tsp"]).toBe("model UpdatedBar {}"); + expect(result.current.files["main.tsp"]).toBe("import './models';"); + }); + + it("creates files from content in single-file mode", () => { + const { result } = renderPlaygroundState({ content: "model Foo {}" }); + + act(() => { + result.current.onFileContentChange("main.tsp", "model UpdatedFoo {}"); + }); + + expect(result.current.files["main.tsp"]).toBe("model UpdatedFoo {}"); + }); + }); +});