From 7d0feba68454c500b7b6aae31c17445050db2fbf Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 9 Nov 2023 15:20:15 -0800 Subject: [PATCH 01/20] Resolve version from query --- packages/website/docusaurus.config.ts | 5 +++-- packages/website/static/playground-version-loader.js | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 packages/website/static/playground-version-loader.js diff --git a/packages/website/docusaurus.config.ts b/packages/website/docusaurus.config.ts index 18dd06ffed..ea295d7751 100644 --- a/packages/website/docusaurus.config.ts +++ b/packages/website/docusaurus.config.ts @@ -68,8 +68,8 @@ const config: Config = { async: true, }, { - src: `https://typespec.blob.core.windows.net/pkgs/indexes/typespec/${latestVersion}.json`, - type: "importmap-shim", + src: `playground-version-loader.js`, + "data-latest-version": latestVersion, }, ], themes: ["@docusaurus/theme-mermaid"], @@ -95,6 +95,7 @@ const config: Config = { ], ], staticDirectories: [ + "static", resolve(__dirname, "./node_modules/@typespec/spec/dist"), resolve(__dirname, "./node_modules/es-module-shims/dist"), ], diff --git a/packages/website/static/playground-version-loader.js b/packages/website/static/playground-version-loader.js new file mode 100644 index 0000000000..a54ed367b9 --- /dev/null +++ b/packages/website/static/playground-version-loader.js @@ -0,0 +1,11 @@ +const latestVersion = document.currentScript.getAttribute("data-latest-version"); +const parsed = new URLSearchParams(window.location.search); + +const requestedVersion = parsed.get("version"); +const importMapUrl = `https://typespec.blob.core.windows.net/pkgs/indexes/typespec/${ + requestedVersion ?? latestVersion +}.json`; +const im = document.createElement("script"); +im.type = "importmap-shim"; +im.src = importMapUrl; +document.currentScript.after(im); From 8933383078dccc10ff7029194a4a5f49f66cef87 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 9 Nov 2023 17:12:06 -0800 Subject: [PATCH 02/20] Customize footer --- common/config/rush/pnpm-lock.yaml | 8 ++++ cspell.yaml | 1 + packages/playground/package.json | 3 +- packages/playground/src/react/footer.tsx | 44 +++---------------- .../src/react/footer/footer.module.css | 18 ++++++++ .../playground/src/react/footer/footer.tsx | 27 ++++++++++++ packages/playground/src/react/index.ts | 1 + packages/playground/src/react/playground.tsx | 22 ++++++++-- packages/website/src/pages/playground.tsx | 21 ++++++++- 9 files changed, 100 insertions(+), 45 deletions(-) create mode 100644 packages/playground/src/react/footer/footer.module.css create mode 100644 packages/playground/src/react/footer/footer.tsx diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 27a8700f29..c2aefd1b36 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -885,6 +885,9 @@ importers: '@typespec/versioning': specifier: workspace:~0.50.0 version: link:../versioning + clsx: + specifier: ~2.0.0 + version: 2.0.0 debounce: specifier: ~1.2.1 version: 1.2.1 @@ -8841,6 +8844,11 @@ packages: engines: {node: '>=6'} dev: false + /clsx@2.0.0: + resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} + engines: {node: '>=6'} + dev: false + /collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} dev: false diff --git a/cspell.yaml b/cspell.yaml index eaed9b9312..03ca0ad1c9 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -13,6 +13,7 @@ words: - cadl - cadleditor - cadleng + - clsx - cobertura - Contoso - createsorreplacesresource diff --git a/packages/playground/package.json b/packages/playground/package.json index d1756b17a2..24c6cbf449 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -87,7 +87,8 @@ "react": "~18.2.0", "swagger-ui-react": "~5.7.2", "vscode-languageserver-textdocument": "~1.0.8", - "vscode-languageserver": "~9.0.0" + "vscode-languageserver": "~9.0.0", + "clsx": "~2.0.0" }, "devDependencies": { "@babel/core": "^7.22.20", diff --git a/packages/playground/src/react/footer.tsx b/packages/playground/src/react/footer.tsx index d4a6bf3550..25264d9f5c 100644 --- a/packages/playground/src/react/footer.tsx +++ b/packages/playground/src/react/footer.tsx @@ -1,11 +1,11 @@ -import { css } from "@emotion/react"; -import { FunctionComponent, PropsWithChildren } from "react"; +import { FunctionComponent } from "react"; import { BrowserHost } from "../types.js"; +import { Footer, FooterItem } from "./footer/footer.js"; export interface FooterProps { host: BrowserHost; } -export const Footer: FunctionComponent = ({ host }) => { +export const DefaultFooter: FunctionComponent = ({ host }) => { const { MANIFEST } = host.compiler; const prItem = MANIFEST.pr ? ( @@ -16,18 +16,7 @@ export const Footer: FunctionComponent = ({ host }) => { <> ); return ( -
+
{prItem} TypeSpec Version @@ -37,29 +26,6 @@ export const Footer: FunctionComponent = ({ host }) => { Commit {MANIFEST.commit.slice(0, 6)} -
+ ); }; - -interface FooterItemProps { - link?: string; -} -const FooterItem: FunctionComponent> = ({ children, link }) => { - return link ? ( - - {children} - - ) : ( -
{children}
- ); -}; - -const FooterItemStyles = css({ - textDecoration: "none", - color: "#fefefe", - borderRight: "1px solid #d5d5d5", - padding: "0 5px", - "&:hover": { - backgroundColor: "#063a5c", - }, -}); diff --git a/packages/playground/src/react/footer/footer.module.css b/packages/playground/src/react/footer/footer.module.css new file mode 100644 index 0000000000..11c5ca67b6 --- /dev/null +++ b/packages/playground/src/react/footer/footer.module.css @@ -0,0 +1,18 @@ +.footer { + grid-area: footer; + display: flex; + font-size: 14px; + + background-color: #007acc; +} + +.footer-item { + text-decoration: none; + color: #fefefe; + border-right: 1px solid #d5d5d5; + padding: 0 5px; +} + +.footer-item:hover { + background-color: #063a5c; +} diff --git a/packages/playground/src/react/footer/footer.tsx b/packages/playground/src/react/footer/footer.tsx new file mode 100644 index 0000000000..6a0d5115a5 --- /dev/null +++ b/packages/playground/src/react/footer/footer.tsx @@ -0,0 +1,27 @@ +import clsx from "clsx"; +import { FunctionComponent, ReactNode } from "react"; +import style from "./footer.module.css"; + +export interface FooterProps { + className?: string; + children: ReactNode; +} + +export const Footer: FunctionComponent = ({ className, children }) => { + return
{children}
; +}; + +export interface FooterItemProps { + link?: string; + children: ReactNode; +} + +export const FooterItem: FunctionComponent = ({ children, link }) => { + return link ? ( + + {children} + + ) : ( +
{children}
+ ); +}; diff --git a/packages/playground/src/react/index.ts b/packages/playground/src/react/index.ts index 013f2ff73f..dc5eafe2b0 100644 --- a/packages/playground/src/react/index.ts +++ b/packages/playground/src/react/index.ts @@ -1,3 +1,4 @@ +export { Footer, FooterItem } from "./footer/footer.js"; export { Playground } from "./playground.js"; export type { PlaygroundProps, PlaygroundSaveData } from "./playground.js"; export { diff --git a/packages/playground/src/react/playground.tsx b/packages/playground/src/react/playground.tsx index 704d85e1e8..17bdabadc5 100644 --- a/packages/playground/src/react/playground.tsx +++ b/packages/playground/src/react/playground.tsx @@ -1,13 +1,21 @@ import { CompilerOptions } from "@typespec/compiler"; import debounce from "debounce"; import { KeyCode, KeyMod, MarkerSeverity, Uri, editor } from "monaco-editor"; -import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + FunctionComponent, + ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { CompletionItemTag } from "vscode-languageserver"; import { getMarkerLocation } from "../services.js"; import { BrowserHost, PlaygroundSample } from "../types.js"; import { EditorCommandBar } from "./editor-command-bar.js"; import { OnMountData, useMonacoModel } from "./editor.js"; -import { Footer } from "./footer.js"; +import { DefaultFooter } from "./footer.js"; import { useControllableValue } from "./hooks.js"; import { OutputView } from "./output-view.js"; import Pane from "./split-pane/pane.js"; @@ -57,6 +65,11 @@ export interface PlaygroundProps { onSave?: (value: PlaygroundSaveData) => void; editorOptions?: PlaygroundEditorsOptions; + + /** + * Change the footer of the playground. + */ + footer?: (data: { host: BrowserHost }) => ReactNode; } export interface PlaygroundEditorsOptions { @@ -212,6 +225,9 @@ export const Playground: FunctionComponent = (props) => { editorRef.current = editor; }, []); + const footer = useMemo(() => { + return props.footer ? props.footer({ host }) : ; + }, [host]); return (
= (props) => { /> -
+ {footer}
); }; diff --git a/packages/website/src/pages/playground.tsx b/packages/website/src/pages/playground.tsx index 1f2197d224..0d333dc056 100644 --- a/packages/website/src/pages/playground.tsx +++ b/packages/website/src/pages/playground.tsx @@ -2,9 +2,10 @@ import BrowserOnly from "@docusaurus/BrowserOnly"; import { useColorMode } from "@docusaurus/theme-common"; import { FluentProvider, webDarkTheme, webLightTheme } from "@fluentui/react-components"; import Layout from "@theme/Layout"; -import { useEffect, useMemo, useState } from "react"; +import type { BrowserHost, PlaygroundSample } from "@typespec/playground"; +import { FunctionComponent, useEffect, useMemo, useState } from "react"; -import { PlaygroundSample } from "@typespec/playground"; +import { Footer, FooterItem } from "@typespec/playground/react"; import "@typespec/playground/style.css"; const libraries = [ @@ -78,6 +79,7 @@ const AsyncPlayground = () => { emitterViewers={{ "@typespec/openapi3": [mod.SwaggerUIViewer] }} importConfig={{ useShim: true }} editorOptions={editorOptions} + footer={({ host }) => } /> ) ); @@ -96,3 +98,18 @@ async function resolvePlaygroundModules(): Promise { return { StandalonePlayground, samples, SwaggerUIViewer } as const; } + +interface PlaygroundFooterProps { + host: BrowserHost; +} + +const PlaygroundFooter: FunctionComponent = ({ host }) => { + return ( +
+ + TypeSpec Version + {host.compiler.MANIFEST.version} + +
+ ); +}; From 680d16b2e479594183837741dbc6e452379bb9b5 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 9 Nov 2023 17:40:06 -0800 Subject: [PATCH 03/20] Change version --- packages/playground/src/browser-host.ts | 7 +- packages/playground/src/types.ts | 3 +- packages/website/src/pages/playground.tsx | 85 ++++++++++++++++++- .../static/playground-version-loader.js | 2 + 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/packages/playground/src/browser-host.ts b/packages/playground/src/browser-host.ts index 1201cd3205..7b53e79a6f 100644 --- a/packages/playground/src/browser-host.ts +++ b/packages/playground/src/browser-host.ts @@ -22,7 +22,12 @@ export async function createBrowserHost( const libraries: Record = {}; for (const libName of libsToLoad) { const { _TypeSpecLibrary_, $lib } = (await importLibrary(libName, importOptions)) as any; - libraries[libName] = { name: libName, isEmitter: $lib?.emitter, definition: $lib }; + libraries[libName] = { + name: libName, + isEmitter: $lib?.emitter, + definition: $lib, + packageJson: JSON.parse(_TypeSpecLibrary_.typespecSourceFiles["package.json"]), + }; for (const [key, value] of Object.entries(_TypeSpecLibrary_.typespecSourceFiles)) { virtualFs.set(`/test/node_modules/${libName}/${key}`, value); } diff --git a/packages/playground/src/types.ts b/packages/playground/src/types.ts index d586be1baf..75014e79e3 100644 --- a/packages/playground/src/types.ts +++ b/packages/playground/src/types.ts @@ -1,4 +1,4 @@ -import { CompilerHost, CompilerOptions, TypeSpecLibrary } from "@typespec/compiler"; +import { CompilerHost, CompilerOptions, NodePackage, TypeSpecLibrary } from "@typespec/compiler"; export interface PlaygroundSample { filename: string; @@ -13,6 +13,7 @@ export interface PlaygroundSample { export interface PlaygroundTspLibrary { name: string; + packageJson: NodePackage; isEmitter: boolean; definition?: TypeSpecLibrary; } diff --git a/packages/website/src/pages/playground.tsx b/packages/website/src/pages/playground.tsx index 0d333dc056..bd1184a173 100644 --- a/packages/website/src/pages/playground.tsx +++ b/packages/website/src/pages/playground.tsx @@ -1,9 +1,25 @@ import BrowserOnly from "@docusaurus/BrowserOnly"; import { useColorMode } from "@docusaurus/theme-common"; -import { FluentProvider, webDarkTheme, webLightTheme } from "@fluentui/react-components"; +import { + FluentProvider, + Popover, + PopoverSurface, + PopoverTrigger, + Select, + SelectOnChangeData, + Table, + TableBody, + TableCell, + TableHeader, + TableHeaderCell, + TableRow, + Title3, + webDarkTheme, + webLightTheme, +} from "@fluentui/react-components"; import Layout from "@theme/Layout"; import type { BrowserHost, PlaygroundSample } from "@typespec/playground"; -import { FunctionComponent, useEffect, useMemo, useState } from "react"; +import { ChangeEvent, FunctionComponent, useEffect, useMemo, useState } from "react"; import { Footer, FooterItem } from "@typespec/playground/react"; import "@typespec/playground/style.css"; @@ -107,9 +123,70 @@ const PlaygroundFooter: FunctionComponent = ({ host }) => return (
- TypeSpec Version - {host.compiler.MANIFEST.version} + + +
+ TypeSpec Version + {host.compiler.MANIFEST.version} +
+
+ + + + +
); }; + +const columns = [ + { columnKey: "name", label: "Library" }, + { columnKey: "version", label: "Version" }, +]; + +const selectedVersion = "0.49.x"; +const versions = ["0.49.x", "0.50.x"]; +const latestRelease = (window as any).TSP_LATEST_RELEASE; +const VersionsPopup: FunctionComponent = ({ host }) => { + return ( +
+
+ Select release + +
+
+ Loaded libraries + + + + {columns.map((column) => ( + {column.label} + ))} + + + + {Object.values(host.libraries).map((item) => ( + + {item.name} + {item.packageJson.version} + + ))} + +
+
+
+ ); +}; + +function changeVersion(ev: ChangeEvent, data: SelectOnChangeData): void { + const query = new URLSearchParams(window.location.search); + query.set("version", data.value); + window.location.href = window.location.pathname + "?" + query.toString(); +} diff --git a/packages/website/static/playground-version-loader.js b/packages/website/static/playground-version-loader.js index a54ed367b9..de5e2b5398 100644 --- a/packages/website/static/playground-version-loader.js +++ b/packages/website/static/playground-version-loader.js @@ -9,3 +9,5 @@ const im = document.createElement("script"); im.type = "importmap-shim"; im.src = importMapUrl; document.currentScript.after(im); + +window.TSP_LATEST_RELEASE = latestVersion; From 95b237a65257de258745caf844f4dbc3f731fbd8 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 9 Nov 2023 17:45:58 -0800 Subject: [PATCH 04/20] Version picker --- packages/website/src/pages/playground.tsx | 14 +++++++++----- .../website/static/playground-version-loader.js | 5 ++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/website/src/pages/playground.tsx b/packages/website/src/pages/playground.tsx index bd1184a173..75721675de 100644 --- a/packages/website/src/pages/playground.tsx +++ b/packages/website/src/pages/playground.tsx @@ -145,18 +145,21 @@ const columns = [ { columnKey: "version", label: "Version" }, ]; -const selectedVersion = "0.49.x"; +interface VersionData { + latest: string; + requested: string; +} +const versionData: VersionData = (window as any).TSP_VERSION_DATA; const versions = ["0.49.x", "0.50.x"]; -const latestRelease = (window as any).TSP_LATEST_RELEASE; const VersionsPopup: FunctionComponent = ({ host }) => { return (
Select release - {versions.map((x) => ( ))} @@ -188,5 +191,6 @@ const VersionsPopup: FunctionComponent = ({ host }) => { function changeVersion(ev: ChangeEvent, data: SelectOnChangeData): void { const query = new URLSearchParams(window.location.search); query.set("version", data.value); - window.location.href = window.location.pathname + "?" + query.toString(); + const newUrl = window.location.pathname + "?" + query.toString(); + window.location.replace(newUrl); } diff --git a/packages/website/static/playground-version-loader.js b/packages/website/static/playground-version-loader.js index de5e2b5398..bcab089670 100644 --- a/packages/website/static/playground-version-loader.js +++ b/packages/website/static/playground-version-loader.js @@ -10,4 +10,7 @@ im.type = "importmap-shim"; im.src = importMapUrl; document.currentScript.after(im); -window.TSP_LATEST_RELEASE = latestVersion; +window.TSP_VERSION_DATA = { + latest: latestVersion, + requested: requestedVersion, +}; From 75d2bf37439b88aefb6bccb7e27c9b20febe08c6 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 9 Nov 2023 18:55:49 -0800 Subject: [PATCH 05/20] Better way to deal with all async stuff --- .../playground-component/playground.tsx | 155 ++++++++++++++++++ packages/website/src/pages/playground.tsx | 154 +---------------- 2 files changed, 162 insertions(+), 147 deletions(-) create mode 100644 packages/website/src/components/playground-component/playground.tsx diff --git a/packages/website/src/components/playground-component/playground.tsx b/packages/website/src/components/playground-component/playground.tsx new file mode 100644 index 0000000000..eabdaabab0 --- /dev/null +++ b/packages/website/src/components/playground-component/playground.tsx @@ -0,0 +1,155 @@ +import { useColorMode } from "@docusaurus/theme-common"; +import { + FluentProvider, + Popover, + PopoverSurface, + PopoverTrigger, + Select, + SelectOnChangeData, + Table, + TableBody, + TableCell, + TableHeader, + TableHeaderCell, + TableRow, + Title3, + webDarkTheme, + webLightTheme, +} from "@fluentui/react-components"; +import Layout from "@theme/Layout"; +import type { BrowserHost } from "@typespec/playground"; +import samples from "@typespec/playground-website/samples"; +import { Footer, FooterItem, StandalonePlayground } from "@typespec/playground/react"; +import { SwaggerUIViewer } from "@typespec/playground/react/viewers"; +import "@typespec/playground/style.css"; +import { ChangeEvent, FunctionComponent, useMemo } from "react"; + +const libraries = [ + "@typespec/compiler", + "@typespec/http", + "@typespec/rest", + "@typespec/openapi", + "@typespec/versioning", + "@typespec/openapi3", + "@typespec/json-schema", + "@typespec/protobuf", +]; +const defaultEmitter = "@typespec/openapi3"; + +export const FluentLayout = ({ children }) => { + return ( + + {children} + + ); +}; + +const FluentWrapper = ({ children }) => { + const { colorMode } = useColorMode(); + + return ( + + {children} + + ); +}; + +export const WebsitePlayground = () => { + const { colorMode } = useColorMode(); + + const editorOptions = useMemo(() => { + return { theme: colorMode === "dark" ? "typespec-dark" : "typespec" }; + }, [colorMode]); + + return ( + } + /> + ); +}; + +interface PlaygroundFooterProps { + host: BrowserHost; +} + +const PlaygroundFooter: FunctionComponent = ({ host }) => { + return ( +
+ + + +
+ TypeSpec Version + {host.compiler.MANIFEST.version} +
+
+ + + + +
+
+
+ ); +}; + +const columns = [ + { columnKey: "name", label: "Library" }, + { columnKey: "version", label: "Version" }, +]; + +interface VersionData { + latest: string; + requested: string; +} +const versionData: VersionData = (window as any).TSP_VERSION_DATA; +const versions = ["0.49.x", "0.50.x"]; +const VersionsPopup: FunctionComponent = ({ host }) => { + return ( +
+
+ Select release + +
+
+ Loaded libraries + + + + {columns.map((column) => ( + {column.label} + ))} + + + + {Object.values(host.libraries).map((item) => ( + + {item.name} + {item.packageJson.version} + + ))} + +
+
+
+ ); +}; + +function changeVersion(ev: ChangeEvent, data: SelectOnChangeData): void { + const query = new URLSearchParams(window.location.search); + query.set("version", data.value); + const newUrl = window.location.pathname + "?" + query.toString(); + window.location.replace(newUrl); +} diff --git a/packages/website/src/pages/playground.tsx b/packages/website/src/pages/playground.tsx index 75721675de..2e7bda430c 100644 --- a/packages/website/src/pages/playground.tsx +++ b/packages/website/src/pages/playground.tsx @@ -1,41 +1,11 @@ import BrowserOnly from "@docusaurus/BrowserOnly"; import { useColorMode } from "@docusaurus/theme-common"; -import { - FluentProvider, - Popover, - PopoverSurface, - PopoverTrigger, - Select, - SelectOnChangeData, - Table, - TableBody, - TableCell, - TableHeader, - TableHeaderCell, - TableRow, - Title3, - webDarkTheme, - webLightTheme, -} from "@fluentui/react-components"; +import { FluentProvider, webDarkTheme, webLightTheme } from "@fluentui/react-components"; import Layout from "@theme/Layout"; -import type { BrowserHost, PlaygroundSample } from "@typespec/playground"; -import { ChangeEvent, FunctionComponent, useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; -import { Footer, FooterItem } from "@typespec/playground/react"; import "@typespec/playground/style.css"; -const libraries = [ - "@typespec/compiler", - "@typespec/http", - "@typespec/rest", - "@typespec/openapi", - "@typespec/versioning", - "@typespec/openapi3", - "@typespec/json-schema", - "@typespec/protobuf", -]; -const defaultEmitter = "@typespec/openapi3"; - export default function PlaygroundPage() { return ( @@ -71,126 +41,16 @@ const FluentWrapper = ({ children }) => { }; const AsyncPlayground = () => { - const { colorMode } = useColorMode(); - - const [mod, setMod] = useState(null); + const [mod, setMod] = useState< + typeof import("../components/playground-component/playground") | null + >(null); useEffect(() => { - resolvePlaygroundModules() + import("../components/playground-component/playground") .then((x) => setMod(x)) .catch((e) => { throw e; }); }, []); - const editorOptions = useMemo(() => { - return { theme: colorMode === "dark" ? "typespec-dark" : "typespec" }; - }, [colorMode]); - - return ( - mod && ( - } - /> - ) - ); -}; - -interface PlaygroundModules { - StandalonePlayground: typeof import("@typespec/playground/react").StandalonePlayground; - samples: Record; - SwaggerUIViewer: typeof import("@typespec/playground/react/viewers").SwaggerUIViewer; -} -async function resolvePlaygroundModules(): Promise { - // Need to import dynamically to avoid SSR issues due to monaco editor referencing navigator. - const { StandalonePlayground } = await import("@typespec/playground/react"); - const { default: samples } = await import("@typespec/playground-website/samples"); - const { SwaggerUIViewer } = await import("@typespec/playground/react/viewers"); - - return { StandalonePlayground, samples, SwaggerUIViewer } as const; -} - -interface PlaygroundFooterProps { - host: BrowserHost; -} - -const PlaygroundFooter: FunctionComponent = ({ host }) => { - return ( -
- - - -
- TypeSpec Version - {host.compiler.MANIFEST.version} -
-
- - - - -
-
-
- ); + return mod && ; }; - -const columns = [ - { columnKey: "name", label: "Library" }, - { columnKey: "version", label: "Version" }, -]; - -interface VersionData { - latest: string; - requested: string; -} -const versionData: VersionData = (window as any).TSP_VERSION_DATA; -const versions = ["0.49.x", "0.50.x"]; -const VersionsPopup: FunctionComponent = ({ host }) => { - return ( -
-
- Select release - -
-
- Loaded libraries - - - - {columns.map((column) => ( - {column.label} - ))} - - - - {Object.values(host.libraries).map((item) => ( - - {item.name} - {item.packageJson.version} - - ))} - -
-
-
- ); -}; - -function changeVersion(ev: ChangeEvent, data: SelectOnChangeData): void { - const query = new URLSearchParams(window.location.search); - query.set("version", data.value); - const newUrl = window.location.pathname + "?" + query.toString(); - window.location.replace(newUrl); -} From 3a370de8a3d9ad2a01bd9ae3d2ba8895c3cf5eb9 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 9 Nov 2023 18:57:24 -0800 Subject: [PATCH 06/20] Fix --- packages/playground/src/react/playground.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/playground/src/react/playground.tsx b/packages/playground/src/react/playground.tsx index 17bdabadc5..767bc256ab 100644 --- a/packages/playground/src/react/playground.tsx +++ b/packages/playground/src/react/playground.tsx @@ -231,10 +231,8 @@ export const Playground: FunctionComponent = (props) => { return (
Date: Thu, 9 Nov 2023 19:01:46 -0800 Subject: [PATCH 07/20] Keep version --- .../website/src/components/playground-component/playground.tsx | 3 ++- packages/website/static/playground-version-loader.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/website/src/components/playground-component/playground.tsx b/packages/website/src/components/playground-component/playground.tsx index eabdaabab0..000eb0070b 100644 --- a/packages/website/src/components/playground-component/playground.tsx +++ b/packages/website/src/components/playground-component/playground.tsx @@ -107,6 +107,7 @@ const columns = [ interface VersionData { latest: string; requested: string; + resolved: string; } const versionData: VersionData = (window as any).TSP_VERSION_DATA; const versions = ["0.49.x", "0.50.x"]; @@ -115,7 +116,7 @@ const VersionsPopup: FunctionComponent = ({ host }) => {
Select release - {versions.map((x) => (
); -}; +}); From 209f465584cac5271fc7edfa89966089288c5e1d Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 10 Nov 2023 14:22:56 -0800 Subject: [PATCH 15/20] Share more --- packages/bundler/src/vite/vite-plugin.ts | 4 ++-- packages/playground-website/package.json | 4 ++++ packages/playground-website/src/index.ts | 16 ++++++++++++++++ packages/playground-website/vite.config.ts | 14 ++------------ packages/playground/src/browser-host.ts | 2 +- packages/playground/src/react/playground.tsx | 2 +- packages/playground/src/react/standalone.tsx | 4 ++-- packages/playground/src/vite/types.ts | 13 ++++++------- .../playground-component/playground.tsx | 18 ++---------------- 9 files changed, 36 insertions(+), 41 deletions(-) create mode 100644 packages/playground-website/src/index.ts diff --git a/packages/bundler/src/vite/vite-plugin.ts b/packages/bundler/src/vite/vite-plugin.ts index b041053d84..4d275b2d62 100644 --- a/packages/bundler/src/vite/vite-plugin.ts +++ b/packages/bundler/src/vite/vite-plugin.ts @@ -9,12 +9,12 @@ import { } from "../bundler.js"; export interface TypeSpecBundlePluginOptions { - folderName: string; + readonly folderName: string; /** * Name of libraries to bundle. */ - libraries: string[]; + readonly libraries: readonly string[]; } export function typespecBundlePlugin(options: TypeSpecBundlePluginOptions): Plugin { diff --git a/packages/playground-website/package.json b/packages/playground-website/package.json index eb71847eeb..86577e22f5 100644 --- a/packages/playground-website/package.json +++ b/packages/playground-website/package.json @@ -19,6 +19,10 @@ ], "type": "module", "exports": { + ".": { + "types": "./dist-dev/src/index.d.ts", + "default": "./dist-dev/src/index.js" + }, "./samples": { "types": "./samples/dist/samples.d.ts", "default": "./samples/dist/samples.js" diff --git a/packages/playground-website/src/index.ts b/packages/playground-website/src/index.ts new file mode 100644 index 0000000000..11f68ed4c8 --- /dev/null +++ b/packages/playground-website/src/index.ts @@ -0,0 +1,16 @@ +import samples from "../samples/dist/samples.js"; + +export const TypeSpecPlaygroundConfig = { + defaultEmitter: "@typespec/openapi3", + libraries: [ + "@typespec/compiler", + "@typespec/http", + "@typespec/rest", + "@typespec/openapi", + "@typespec/versioning", + "@typespec/openapi3", + "@typespec/json-schema", + "@typespec/protobuf", + ], + samples, +} as const; diff --git a/packages/playground-website/vite.config.ts b/packages/playground-website/vite.config.ts index 4e63feafa5..ae05e3067e 100644 --- a/packages/playground-website/vite.config.ts +++ b/packages/playground-website/vite.config.ts @@ -1,23 +1,13 @@ import { definePlaygroundViteConfig } from "@typespec/playground/vite"; import { visualizer } from "rollup-plugin-visualizer"; import { defineConfig, loadEnv } from "vite"; +import { TypeSpecPlaygroundConfig } from "./src/index.js"; export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd()); const useLocalLibraries = env["VITE_USE_LOCAL_LIBRARIES"] === "true"; const config = definePlaygroundViteConfig({ - defaultEmitter: "@typespec/openapi3", - libraries: [ - "@typespec/compiler", - "@typespec/http", - "@typespec/rest", - "@typespec/openapi", - "@typespec/versioning", - "@typespec/openapi3", - "@typespec/json-schema", - "@typespec/protobuf", - ], - enableSwaggerUI: true, + ...TypeSpecPlaygroundConfig, links: { githubIssueUrl: `https://github.com/microsoft/typespec/issues/new`, documentationUrl: "https://microsoft.github.io/typespec", diff --git a/packages/playground/src/browser-host.ts b/packages/playground/src/browser-host.ts index 7b53e79a6f..f5b2c9970e 100644 --- a/packages/playground/src/browser-host.ts +++ b/packages/playground/src/browser-host.ts @@ -13,7 +13,7 @@ export function resolveVirtualPath(path: string, ...paths: string[]) { * @returns */ export async function createBrowserHost( - libsToLoad: string[], + libsToLoad: readonly string[], importOptions: LibraryImportOptions = {} ): Promise { const virtualFs = new Map(); diff --git a/packages/playground/src/react/playground.tsx b/packages/playground/src/react/playground.tsx index 6dcce9c577..0d35a8cddf 100644 --- a/packages/playground/src/react/playground.tsx +++ b/packages/playground/src/react/playground.tsx @@ -31,7 +31,7 @@ export interface PlaygroundProps { defaultContent?: string; /** List of available libraries */ - libraries: string[]; + readonly libraries: readonly string[]; /** Emitter to use */ emitter?: string; diff --git a/packages/playground/src/react/standalone.tsx b/packages/playground/src/react/standalone.tsx index 2fa0a890dd..ca50361ad4 100644 --- a/packages/playground/src/react/standalone.tsx +++ b/packages/playground/src/react/standalone.tsx @@ -17,8 +17,8 @@ import { BrowserHost } from "../types.js"; import { Playground, PlaygroundProps, PlaygroundSaveData } from "./playground.js"; export interface ReactPlaygroundConfig extends Partial { - libraries: string[]; - importConfig?: LibraryImportOptions; + readonly libraries: readonly string[]; + readonly importConfig?: LibraryImportOptions; } interface StandalonePlaygroundContext { diff --git a/packages/playground/src/vite/types.ts b/packages/playground/src/vite/types.ts index c3af1bdd12..cc560b31d9 100644 --- a/packages/playground/src/vite/types.ts +++ b/packages/playground/src/vite/types.ts @@ -6,14 +6,13 @@ export interface PlaygroundUserConfig extends Omit /** * If the bundle library plugin should be loaded. */ - skipBundleLibraries?: boolean; - samples?: Record; + readonly skipBundleLibraries?: boolean; + readonly samples?: Record; } export interface PlaygroundConfig { - defaultEmitter: string; - libraries: string[]; - samples: Record; - enableSwaggerUI: boolean; - links?: PlaygroundLinks; + readonly defaultEmitter: string; + readonly libraries: readonly string[]; + readonly samples: Record; + readonly links?: PlaygroundLinks; } diff --git a/packages/website/src/components/playground-component/playground.tsx b/packages/website/src/components/playground-component/playground.tsx index 613784a352..75faf4b7ee 100644 --- a/packages/website/src/components/playground-component/playground.tsx +++ b/packages/website/src/components/playground-component/playground.tsx @@ -1,7 +1,7 @@ import { useColorMode } from "@docusaurus/theme-common"; import { FluentProvider, webDarkTheme, webLightTheme } from "@fluentui/react-components"; import Layout from "@theme/Layout"; -import samples from "@typespec/playground-website/samples"; +import { TypeSpecPlaygroundConfig } from "@typespec/playground-website"; import { Footer, FooterVersionItem, @@ -15,18 +15,6 @@ import { FunctionComponent, useMemo } from "react"; import "@typespec/playground/style.css"; import { VersionData } from "./import-map"; -const libraries = [ - "@typespec/compiler", - "@typespec/http", - "@typespec/rest", - "@typespec/openapi", - "@typespec/versioning", - "@typespec/openapi3", - "@typespec/json-schema", - "@typespec/protobuf", -]; -const defaultEmitter = "@typespec/openapi3"; - export const FluentLayout = ({ children }) => { return ( @@ -58,9 +46,7 @@ export const WebsitePlayground = ({ versionData }: WebsitePlaygroundProps) => { return ( Date: Fri, 10 Nov 2023 14:40:49 -0800 Subject: [PATCH 16/20] Load from json file and auto bump --- .../.scripts/update-playground-versions.mjs | 31 +++++++++++++++++++ packages/website/package.json | 3 +- packages/website/playground-versions.json | 1 + .../playground-component/playground.tsx | 4 +-- 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 packages/website/.scripts/update-playground-versions.mjs create mode 100644 packages/website/playground-versions.json diff --git a/packages/website/.scripts/update-playground-versions.mjs b/packages/website/.scripts/update-playground-versions.mjs new file mode 100644 index 0000000000..b15a5daee9 --- /dev/null +++ b/packages/website/.scripts/update-playground-versions.mjs @@ -0,0 +1,31 @@ +// @ts-check +import { readFile, writeFile } from "fs/promises"; +import { dirname, resolve } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const filename = resolve(__dirname, "../playground-versions.json"); +const currentContent = await readFile(filename); +const current = JSON.parse(currentContent.toString()); + +console.log("Current versions:", current); + +async function getMajorMinorVersion() { + const version = JSON.parse( + (await readFile(resolve(__dirname, "../../compiler/package.json"))).toString() + ).version; + const [major, minor] = version.split("."); + return `${major}.${minor}.x`; +} + +const latestVersion = await getMajorMinorVersion(); + +if (current.includes(latestVersion)) { + console.log(`Latest version ${latestVersion} is already in the list.`); + process.exit(0); +} + +const newVersions = [latestVersion, ...current]; +console.log("New versions: ", newVersions); +const newContent = JSON.stringify(newVersions, null, 2); +await writeFile(filename, newContent); diff --git a/packages/website/package.json b/packages/website/package.json index c76d258d96..ac55d04b2f 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -13,7 +13,8 @@ "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", - "update-latest-docs": "rimraf versions.json ./versioned_docs ./versioned_sidebars && docusaurus docs:version latest", + "update-latest-docs": "rimraf versions.json ./versioned_docs ./versioned_sidebars && docusaurus docs:version latest && npm run update-playground-versions", + "update-playground-versions": "node ./.scripts/update-playground-versions.mjs", "lint": "eslint . --ext .ts,.js --max-warnings=0", "lint:fix": "eslint . --fix --ext .ts,.js", "regen-docs": "node ./.scripts/regen-compiler-docs.mjs", diff --git a/packages/website/playground-versions.json b/packages/website/playground-versions.json new file mode 100644 index 0000000000..2248eafe88 --- /dev/null +++ b/packages/website/playground-versions.json @@ -0,0 +1 @@ +["0.50.x", "0.49.x"] diff --git a/packages/website/src/components/playground-component/playground.tsx b/packages/website/src/components/playground-component/playground.tsx index 75faf4b7ee..3da87b7a86 100644 --- a/packages/website/src/components/playground-component/playground.tsx +++ b/packages/website/src/components/playground-component/playground.tsx @@ -1,5 +1,6 @@ import { useColorMode } from "@docusaurus/theme-common"; import { FluentProvider, webDarkTheme, webLightTheme } from "@fluentui/react-components"; +import versions from "@site/playground-versions.json" assert { type: "json" }; import Layout from "@theme/Layout"; import { TypeSpecPlaygroundConfig } from "@typespec/playground-website"; import { @@ -11,9 +12,9 @@ import { } from "@typespec/playground/react"; import { SwaggerUIViewer } from "@typespec/playground/react/viewers"; import { FunctionComponent, useMemo } from "react"; +import { VersionData } from "./import-map"; import "@typespec/playground/style.css"; -import { VersionData } from "./import-map"; export const FluentLayout = ({ children }) => { return ( @@ -58,7 +59,6 @@ export const WebsitePlayground = ({ versionData }: WebsitePlaygroundProps) => { interface PlaygroundFooterProps { versionData: VersionData; } -const versions = ["0.50.x", "0.49.x"]; const PlaygroundFooter: FunctionComponent = ({ versionData }) => { const versionSelectorProps: VersionSelectorProps = useMemo(() => { From f5539a312e5a3d85a078d4b1a4c01da0ce3df7cb Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 10 Nov 2023 14:47:51 -0800 Subject: [PATCH 17/20] . --- .../feature-version-picker_2023-11-10-22-47.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@typespec/bundler/feature-version-picker_2023-11-10-22-47.json diff --git a/common/changes/@typespec/bundler/feature-version-picker_2023-11-10-22-47.json b/common/changes/@typespec/bundler/feature-version-picker_2023-11-10-22-47.json new file mode 100644 index 0000000000..aa408d584a --- /dev/null +++ b/common/changes/@typespec/bundler/feature-version-picker_2023-11-10-22-47.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@typespec/bundler", + "comment": "", + "type": "none" + } + ], + "packageName": "@typespec/bundler" +} \ No newline at end of file From 72f25a72c94cb81797bc053dbc472d45d7467b0e Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 13 Nov 2023 08:56:27 -0800 Subject: [PATCH 18/20] Default footer --- packages/playground-website/src/main.tsx | 36 ++++++++++++++++++- packages/playground-website/src/style.css | 4 +++ packages/playground-website/vite.config.ts | 18 ++++++++++ .../playground/src/react/default-footer.tsx | 22 ++---------- .../src/react/footer/footer-version-item.tsx | 2 +- packages/playground/src/react/playground.tsx | 2 +- packages/website/docusaurus.config.ts | 1 - 7 files changed, 61 insertions(+), 24 deletions(-) diff --git a/packages/playground-website/src/main.tsx b/packages/playground-website/src/main.tsx index 3400cb126e..41b77fe036 100644 --- a/packages/playground-website/src/main.tsx +++ b/packages/playground-website/src/main.tsx @@ -1,14 +1,47 @@ import { registerMonacoDefaultWorkersForVite } from "@typespec/playground"; import PlaygroundManifest from "@typespec/playground/manifest"; -import { renderReactPlayground } from "@typespec/playground/react"; +import { + Footer, + FooterItem, + FooterVersionItem, + renderReactPlayground, +} from "@typespec/playground/react"; import { SwaggerUIViewer } from "@typespec/playground/react/viewers"; import samples from "../samples/dist/samples.js"; +import { MANIFEST } from "@typespec/compiler"; import "@typespec/playground/style.css"; import "./style.css"; registerMonacoDefaultWorkersForVite(); +declare const __PR__: string | undefined; +declare const __COMMIT_HASH__: string | undefined; + +const commit = typeof __COMMIT_HASH__ !== "undefined" ? __COMMIT_HASH__ : undefined; +const pr = typeof __PR__ !== "undefined" ? __PR__ : undefined; +const PlaygroundFooter = () => { + const prItem = pr ? ( + + PR + {pr} + + ) : ( + <> + ); + + return ( +
+ {prItem} + + + Commit + {MANIFEST.commit.slice(0, 6)} + +
+ ); +}; + await renderReactPlayground({ ...PlaygroundManifest, samples, @@ -18,4 +51,5 @@ await renderReactPlayground({ importConfig: { useShim: true, }, + footer: , }); diff --git a/packages/playground-website/src/style.css b/packages/playground-website/src/style.css index ea1e941c36..79c8c2046c 100644 --- a/packages/playground-website/src/style.css +++ b/packages/playground-website/src/style.css @@ -2,3 +2,7 @@ body { margin: 0; padding: 0; } + +.pr-footer { + background-color: #ce662a; +} diff --git a/packages/playground-website/vite.config.ts b/packages/playground-website/vite.config.ts index ae05e3067e..57ec49f55d 100644 --- a/packages/playground-website/vite.config.ts +++ b/packages/playground-website/vite.config.ts @@ -1,8 +1,18 @@ import { definePlaygroundViteConfig } from "@typespec/playground/vite"; +import { execSync } from "child_process"; import { visualizer } from "rollup-plugin-visualizer"; import { defineConfig, loadEnv } from "vite"; import { TypeSpecPlaygroundConfig } from "./src/index.js"; +function getCommit() { + return execSync("git rev-parse HEAD").toString().trim(); +} + +function getPrNumber() { + // Set by Azure DevOps. + return process.env["SYSTEM_PULLREQUEST_PULLREQUESTNUMBER"]; +} + export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd()); const useLocalLibraries = env["VITE_USE_LOCAL_LIBRARIES"] === "true"; @@ -20,5 +30,13 @@ export default defineConfig(({ mode }) => { filename: "temp/stats.html", }) as any ); + + const prNumber = getPrNumber(); + if (prNumber) { + config.define = { + __PR__: JSON.stringify(prNumber), + __COMMIT_HASH__: JSON.stringify(getCommit()), + }; + } return config; }); diff --git a/packages/playground/src/react/default-footer.tsx b/packages/playground/src/react/default-footer.tsx index 7be358ce2a..c551ffc44d 100644 --- a/packages/playground/src/react/default-footer.tsx +++ b/packages/playground/src/react/default-footer.tsx @@ -1,29 +1,11 @@ import { FunctionComponent } from "react"; -import { BrowserHost } from "../types.js"; import { FooterVersionItem } from "./footer/footer-version-item.js"; -import { Footer, FooterItem } from "./footer/index.js"; +import { Footer } from "./footer/index.js"; -export interface FooterProps { - host: BrowserHost; -} -export const DefaultFooter: FunctionComponent = ({ host }) => { - const { MANIFEST } = host.compiler; - const prItem = MANIFEST.pr ? ( - - PR - {MANIFEST.pr} - - ) : ( - <> - ); +export const DefaultFooter: FunctionComponent = () => { return (
- {prItem} - - Commit - {MANIFEST.commit.slice(0, 6)} -
); }; diff --git a/packages/playground/src/react/footer/footer-version-item.tsx b/packages/playground/src/react/footer/footer-version-item.tsx index 167ce4ae7c..95ee17f529 100644 --- a/packages/playground/src/react/footer/footer-version-item.tsx +++ b/packages/playground/src/react/footer/footer-version-item.tsx @@ -50,7 +50,7 @@ export const FooterVersionItem = memo(({ versionSelector }: FooterVersionItemPro
Version {selected} - {latest && latest === selected ? " (latest)" : " (old)"} + {latest ? (latest === selected ? " (latest)" : " (old)") : ""}
diff --git a/packages/playground/src/react/playground.tsx b/packages/playground/src/react/playground.tsx index 0d35a8cddf..3ba90a5f61 100644 --- a/packages/playground/src/react/playground.tsx +++ b/packages/playground/src/react/playground.tsx @@ -272,7 +272,7 @@ export const Playground: FunctionComponent = (props) => { /> - {props.footer ?? } + {props.footer ?? }
); diff --git a/packages/website/docusaurus.config.ts b/packages/website/docusaurus.config.ts index 1e00943ab3..c47e0c28b7 100644 --- a/packages/website/docusaurus.config.ts +++ b/packages/website/docusaurus.config.ts @@ -82,7 +82,6 @@ const config: Config = { { tagName: "script", attributes: { - // cspell:ignore esms type: "playground-options", }, innerHTML: JSON.stringify({ From badcbed54e8f5fbb98725c9ae996967ca77cd433 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 13 Nov 2023 09:10:04 -0800 Subject: [PATCH 19/20] Keep existing state --- packages/playground/src/state-storage.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/playground/src/state-storage.ts b/packages/playground/src/state-storage.ts index 3d3709503f..3352576952 100644 --- a/packages/playground/src/state-storage.ts +++ b/packages/playground/src/state-storage.ts @@ -78,14 +78,16 @@ export function createUrlStateStorage( } function save(data: T) { - const params = new URLSearchParams(); + const params = new URLSearchParams(location.search); for (const [key, item] of Object.entries(schema)) { const value = (data as any)[key]; if (value) { const serialized = serialize(item, value); const compressed = compress(item, serialized); - params.append(item.queryParam, compressed); + params.set(item.queryParam, compressed); + } else { + params.delete(item.queryParam); } } history.pushState(null, "", window.location.pathname + "?" + params.toString()); From 860f27e207d867f0d182fe0447020f4323881c6b Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 13 Nov 2023 09:17:18 -0800 Subject: [PATCH 20/20] Tweak style --- .../src/react/footer/footer-version-item.module.css | 8 ++++++++ .../playground/src/react/footer/footer-version-item.tsx | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 packages/playground/src/react/footer/footer-version-item.module.css diff --git a/packages/playground/src/react/footer/footer-version-item.module.css b/packages/playground/src/react/footer/footer-version-item.module.css new file mode 100644 index 0000000000..050988e6e4 --- /dev/null +++ b/packages/playground/src/react/footer/footer-version-item.module.css @@ -0,0 +1,8 @@ +.version-item { + padding: 0; +} +.button { + cursor: pointer; + height: 100%; + padding: 0 5px; +} diff --git a/packages/playground/src/react/footer/footer-version-item.tsx b/packages/playground/src/react/footer/footer-version-item.tsx index 95ee17f529..488168de73 100644 --- a/packages/playground/src/react/footer/footer-version-item.tsx +++ b/packages/playground/src/react/footer/footer-version-item.tsx @@ -15,6 +15,7 @@ import { import { ChangeEvent, FunctionComponent, memo, useCallback } from "react"; import { usePlaygroundContext } from "../context/playground-context.js"; import { FooterItem } from "./footer-item.js"; +import style from "./footer-version-item.module.css"; export interface VersionSelectorVersion { name: string; @@ -44,10 +45,10 @@ export const FooterVersionItem = memo(({ versionSelector }: FooterVersionItemPro const latest = versionSelector?.latest; const selected = versionSelector?.selected ?? host.compiler.MANIFEST.version; return ( - + -
+
Version {selected} {latest ? (latest === selected ? " (latest)" : " (old)") : ""}