├── public └── .gitkeep ├── .node-version ├── .github ├── FUNDING.yml ├── renovate.json └── workflows │ └── ci.yml ├── src ├── vite-env.d.ts ├── ui │ ├── input │ │ ├── index.ts │ │ └── Input.vue │ ├── label │ │ ├── index.ts │ │ └── Label.vue │ ├── switch │ │ ├── index.ts │ │ └── Switch.vue │ ├── checkbox │ │ ├── index.ts │ │ └── Checkbox.vue │ ├── radio-group │ │ ├── index.ts │ │ ├── RadioGroup.vue │ │ └── RadioGroupItem.vue │ ├── splitter │ │ ├── index.ts │ │ ├── Splitter.vue │ │ ├── SplitterPanel.vue │ │ └── SplitterResizeHandle.vue │ ├── tabs │ │ ├── index.ts │ │ ├── Tabs.vue │ │ ├── TabsList.vue │ │ ├── TabsContent.vue │ │ └── TabsTrigger.vue │ ├── dialog │ │ ├── DialogClose.vue │ │ ├── DialogTrigger.vue │ │ ├── DialogHeader.vue │ │ ├── DialogFooter.vue │ │ ├── Dialog.vue │ │ ├── index.ts │ │ ├── DialogTitle.vue │ │ ├── DialogDescription.vue │ │ ├── DialogContent.vue │ │ └── DialogScrollContent.vue │ ├── select │ │ ├── SelectValue.vue │ │ ├── SelectItemText.vue │ │ ├── SelectLabel.vue │ │ ├── Select.vue │ │ ├── SelectGroup.vue │ │ ├── SelectSeparator.vue │ │ ├── index.ts │ │ ├── SelectScrollUpButton.vue │ │ ├── SelectScrollDownButton.vue │ │ ├── SelectTrigger.vue │ │ ├── SelectItem.vue │ │ └── SelectContent.vue │ ├── navigation-menu │ │ ├── NavigationMenuItem.vue │ │ ├── NavigationMenuLink.vue │ │ ├── NavigationMenuList.vue │ │ ├── NavigationMenu.vue │ │ ├── NavigationMenuTrigger.vue │ │ ├── index.ts │ │ ├── NavigationMenuIndicator.vue │ │ ├── NavigationMenuViewport.vue │ │ └── NavigationMenuContent.vue │ └── button │ │ ├── Button.vue │ │ └── index.ts ├── global.css ├── utils │ ├── cn.ts │ ├── constants.ts │ ├── range.ts │ ├── url.ts │ ├── viz.ts │ └── shiki.ts ├── main.ts ├── components │ ├── output │ │ ├── RustAstPanel.vue │ │ ├── ScopePanel.vue │ │ ├── DiagnosticPanel.vue │ │ ├── SymbolPanel.vue │ │ ├── EsTreePanel.vue │ │ ├── CodegenPanel.vue │ │ ├── OutputPreview.vue │ │ ├── OutputPanel.vue │ │ ├── ControlflowPanel.vue │ │ └── FormatterPanel.vue │ ├── ast │ │ ├── Brackets.vue │ │ ├── SummaryValue.vue │ │ ├── Value.vue │ │ └── Property.vue │ ├── sidebar │ │ ├── Linter.vue │ │ ├── Logo.vue │ │ ├── Codegen.vue │ │ ├── Define.vue │ │ ├── Sidebar.vue │ │ ├── OptionsDialog.vue │ │ ├── Minifier.vue │ │ ├── Transformer.vue │ │ └── Parser.vue │ ├── header │ │ ├── Header.vue │ │ ├── Media.vue │ │ ├── Switch.vue │ │ └── Nav.vue │ ├── ui │ │ ├── Checkbox.vue │ │ └── Select.vue │ ├── CopyContainer.vue │ ├── input │ │ └── InputEditor.vue │ ├── IoContainer.vue │ └── MonacoEditor.vue ├── composables │ ├── state.ts │ ├── editor.worker.ts │ └── oxc.ts ├── init.ts └── App.vue ├── .vscode └── extensions.json ├── .gitignore ├── eslint.config.js ├── _headers ├── README.md ├── components.json ├── tsconfig.json ├── index.html ├── CLAUDE.md ├── unocss.config.ts ├── LICENSE ├── package.json └── vite.config.ts /public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Boshen, sxzz] 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Input } from "./Input.vue"; 2 | -------------------------------------------------------------------------------- /src/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Label } from "./Label.vue"; 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /src/ui/switch/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Switch } from "./Switch.vue"; 2 | -------------------------------------------------------------------------------- /src/ui/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Checkbox } from "./Checkbox.vue"; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .vite/ 4 | tsconfig.tsbuildinfo 5 | .eslintcache 6 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { sxzz } from "@sxzz/eslint-config"; 2 | export default sxzz({ 3 | prettier: false, 4 | }); 5 | -------------------------------------------------------------------------------- /_headers: -------------------------------------------------------------------------------- 1 | /* 2 | X-Frame-Options: DENY 3 | Cross-Origin-Opener-Policy: same-origin 4 | Cross-Origin-Embedder-Policy: require-corp 5 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>Boshen/renovate"] 4 | } 5 | -------------------------------------------------------------------------------- /src/global.css: -------------------------------------------------------------------------------- 1 | .ast-highlight { 2 | --at-apply: "bg-blue-400/30 dark:bg-blue-400/20"; 3 | } 4 | 5 | .dark { 6 | color-scheme: dark; 7 | } 8 | -------------------------------------------------------------------------------- /src/ui/radio-group/index.ts: -------------------------------------------------------------------------------- 1 | export { default as RadioGroup } from "./RadioGroup.vue"; 2 | export { default as RadioGroupItem } from "./RadioGroupItem.vue"; 3 | -------------------------------------------------------------------------------- /src/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/ui/splitter/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Splitter } from "./Splitter.vue"; 2 | export { default as SplitterPanel } from "./SplitterPanel.vue"; 3 | export { default as SplitterResizeHandle } from "./SplitterResizeHandle.vue"; 4 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import "./init"; 4 | 5 | import "@unocss/reset/tailwind.css"; 6 | import "uno.css"; 7 | import "./global.css"; 8 | 9 | createApp(App).mount("#app"); 10 | -------------------------------------------------------------------------------- /src/ui/tabs/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tabs } from "./Tabs.vue"; 2 | export { default as TabsContent } from "./TabsContent.vue"; 3 | export { default as TabsList } from "./TabsList.vue"; 4 | export { default as TabsTrigger } from "./TabsTrigger.vue"; 5 | -------------------------------------------------------------------------------- /src/components/output/RustAstPanel.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /src/ui/dialog/DialogClose.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/ui/select/SelectValue.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/components/output/ScopePanel.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /src/ui/dialog/DialogTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/ui/select/SelectItemText.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Oxc Playground 2 | 3 | [playground.oxc.rs](https://playground.oxc.rs) 4 | 5 | ## Development 6 | 7 | Assuming the oxc repository is in `../oxc`: 8 | 9 | - in a different terminal and in the `oxc` repository: 10 | - `just install-wasm` 11 | - `just watch-playground` 12 | - in this repo: `pnpm run dev` 13 | -------------------------------------------------------------------------------- /src/components/output/DiagnosticPanel.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/ui/navigation-menu/NavigationMenuItem.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/ui/dialog/DialogHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /src/components/output/SymbolPanel.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /src/components/ast/Brackets.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-vue.com/schema.json", 3 | "style": "new-york", 4 | "typescript": true, 5 | "tailwind": { 6 | "config": "tailwind.config.js", 7 | "css": "src/assets/index.css", 8 | "baseColor": "slate", 9 | "cssVariables": true 10 | }, 11 | "framework": "vite", 12 | "aliases": { 13 | "components": "~/components", 14 | "utils": "~/utils/cn", 15 | "ui": "~/ui" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/composables/state.ts: -------------------------------------------------------------------------------- 1 | import { useDark } from "@vueuse/core"; 2 | import { ref } from "vue"; 3 | import type { Range } from "~/utils/range"; 4 | 5 | export const dark = useDark(); 6 | export const editorValue = ref(""); 7 | 8 | export const editorCursor = ref(0); 9 | export const autoFocus = ref(true); 10 | export const outputHoverRange = ref(); 11 | 12 | // Active tab state for output panel 13 | export const activeTab = ref("codegen"); 14 | -------------------------------------------------------------------------------- /src/ui/tabs/Tabs.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /src/ui/dialog/DialogFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /src/ui/select/SelectLabel.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /src/ui/dialog/Dialog.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /src/ui/select/Select.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /src/components/sidebar/Linter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const PLAYGROUND_DEMO_CODE = ` 2 | import React, { useEffect, useRef } from 'react' 3 | 4 | const DummyComponent: React.FC = () => { 5 | const ref = useRef(null) 6 | 7 | useEffect(() => { 8 | if (ref.current) ref.current.focus() 9 | }, []) 10 | 11 | return ( 12 |
{Boolean(ref.current) ?? ( 13 | 14 | )} 15 |
16 | ) 17 | } 18 | 19 | export default DummyComponent 20 | `.trim(); 21 | -------------------------------------------------------------------------------- /src/ui/splitter/Splitter.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /src/ui/splitter/SplitterPanel.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /src/utils/range.ts: -------------------------------------------------------------------------------- 1 | import { editorCursor } from "~/composables/state"; 2 | 3 | export type Range = [start: number, end: number]; 4 | 5 | export function getRange(node: unknown): [number, number] | undefined { 6 | if (node && typeof node === "object" && "start" in node && "end" in node) { 7 | return [node.start as number, node.end as number]; 8 | } 9 | } 10 | 11 | export function checkRange(range?: Range) { 12 | if (!range) return false; 13 | return range[0] <= editorCursor.value && range[1] > editorCursor.value; 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "ES2020", 5 | "jsx": "preserve", 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 7 | "module": "ESNext", 8 | "moduleResolution": "bundler", 9 | "paths": { 10 | "~/*": ["./src/*"] 11 | }, 12 | "strict": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "skipLibCheck": true 16 | }, 17 | "include": ["vite.config.ts", "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], 18 | "exclude": ["node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /src/components/header/Header.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /src/ui/navigation-menu/NavigationMenuLink.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Oxc - The JavaScript Oxidation Compiler 8 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Dialog } from "./Dialog.vue"; 2 | export { default as DialogClose } from "./DialogClose.vue"; 3 | export { default as DialogContent } from "./DialogContent.vue"; 4 | export { default as DialogDescription } from "./DialogDescription.vue"; 5 | export { default as DialogFooter } from "./DialogFooter.vue"; 6 | export { default as DialogHeader } from "./DialogHeader.vue"; 7 | export { default as DialogScrollContent } from "./DialogScrollContent.vue"; 8 | export { default as DialogTitle } from "./DialogTitle.vue"; 9 | export { default as DialogTrigger } from "./DialogTrigger.vue"; 10 | -------------------------------------------------------------------------------- /src/ui/select/SelectGroup.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /src/ui/select/SelectSeparator.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /src/composables/editor.worker.ts: -------------------------------------------------------------------------------- 1 | import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; 2 | import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; 3 | import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; 4 | 5 | // @ts-ignore 6 | globalThis.MonacoEnvironment = { 7 | globalAPI: true, 8 | getWorker(_: any, label: string) { 9 | if (label === "json") { 10 | return new jsonWorker(); 11 | } 12 | if (label === "typescript" || label === "javascript") { 13 | return new tsWorker(); 14 | } 15 | return new editorWorker(); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # Project: Oxc Playground 2 | 3 | ## Commands 4 | 5 | ### Development 6 | 7 | ```bash 8 | pnpm dev # Start Vite dev server 9 | ``` 10 | 11 | ### Build 12 | 13 | ```bash 14 | pnpm build # Run TypeScript checks and build with Vite 15 | ``` 16 | 17 | ### Preview 18 | 19 | ```bash 20 | pnpm preview # Preview production build locally 21 | ``` 22 | 23 | ### Code Quality 24 | 25 | ```bash 26 | pnpm lint # Run ESLint with cache 27 | pnpm typecheck # Run vue-tsc for type checking 28 | pnpm format # Run oxfmt formatter 29 | ``` 30 | 31 | ## Project Info 32 | 33 | - Node requirement: >=20.14.0 34 | - Package manager: pnpm@10.15.1 35 | - Uses git hooks for pre-commit linting 36 | -------------------------------------------------------------------------------- /src/ui/label/Label.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 28 | -------------------------------------------------------------------------------- /src/ui/button/Button.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /src/ui/tabs/TabsList.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 28 | -------------------------------------------------------------------------------- /src/ui/select/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Select } from "./Select.vue"; 2 | export { default as SelectContent } from "./SelectContent.vue"; 3 | export { default as SelectGroup } from "./SelectGroup.vue"; 4 | export { default as SelectItem } from "./SelectItem.vue"; 5 | export { default as SelectItemText } from "./SelectItemText.vue"; 6 | export { default as SelectLabel } from "./SelectLabel.vue"; 7 | export { default as SelectScrollDownButton } from "./SelectScrollDownButton.vue"; 8 | export { default as SelectScrollUpButton } from "./SelectScrollUpButton.vue"; 9 | export { default as SelectSeparator } from "./SelectSeparator.vue"; 10 | export { default as SelectTrigger } from "./SelectTrigger.vue"; 11 | export { default as SelectValue } from "./SelectValue.vue"; 12 | -------------------------------------------------------------------------------- /src/init.ts: -------------------------------------------------------------------------------- 1 | import * as monaco from "monaco-editor"; 2 | monaco.json.jsonDefaults.setDiagnosticsOptions({ 3 | allowComments: true, 4 | enableSchemaRequest: true, 5 | trailingCommas: "ignore", 6 | }); 7 | 8 | monaco.typescript.typescriptDefaults.setDiagnosticsOptions({ 9 | noSemanticValidation: true, 10 | noSyntaxValidation: true, 11 | noSuggestionDiagnostics: true, 12 | }); 13 | monaco.typescript.typescriptDefaults.setCompilerOptions({ 14 | allowJs: true, 15 | target: monaco.typescript.ScriptTarget.ESNext, 16 | module: monaco.typescript.ModuleKind.ESNext, 17 | allowNonTsExtensions: true, 18 | moduleResolution: monaco.typescript.ModuleResolutionKind.NodeJs, 19 | noEmit: true, 20 | esModuleInterop: true, 21 | jsx: monaco.typescript.JsxEmit.React, 22 | }); 23 | -------------------------------------------------------------------------------- /src/ui/dialog/DialogTitle.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | -------------------------------------------------------------------------------- /src/components/ui/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 31 | -------------------------------------------------------------------------------- /src/ui/dialog/DialogDescription.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 31 | -------------------------------------------------------------------------------- /src/utils/url.ts: -------------------------------------------------------------------------------- 1 | import { strFromU8, strToU8, unzlibSync, zlibSync } from "fflate"; 2 | 3 | export function utoa(data: string): string { 4 | const buffer = strToU8(data); 5 | const zipped = zlibSync(buffer, { level: 9 }); 6 | const binary = strFromU8(zipped, true); 7 | return btoa(binary); 8 | } 9 | 10 | export function atou(base64: string): string { 11 | const binary = atob(base64); 12 | 13 | // zlib header (x78), level 9 (xDA) 14 | if (binary.startsWith("\u0078\u00DA")) { 15 | const buffer = strToU8(binary, true); 16 | const unzipped = unzlibSync(buffer); 17 | return strFromU8(unzipped); 18 | } 19 | 20 | // old unicode hacks for backward compatibility 21 | // https://base64.guru/developers/javascript/examples/unicode-strings 22 | return decodeURIComponent(escape(binary)); 23 | } 24 | -------------------------------------------------------------------------------- /src/ui/tabs/TabsContent.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | -------------------------------------------------------------------------------- /src/ui/radio-group/RadioGroup.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /unocss.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | presetAttributify, 4 | presetIcons, 5 | presetWind3, 6 | transformerDirectives, 7 | } from "unocss"; 8 | import presetAnimations from "unocss-preset-animations"; 9 | import { presetShadcn } from "unocss-preset-shadcn"; 10 | 11 | export default defineConfig({ 12 | presets: [ 13 | presetWind3(), 14 | presetAnimations(), 15 | presetShadcn({ 16 | color: "blue", 17 | darkSelector: ".dark", 18 | }), 19 | presetIcons({ scale: 1.2 }), 20 | presetAttributify(), 21 | ], 22 | transformers: [transformerDirectives()], 23 | content: { 24 | pipeline: { 25 | include: [ 26 | // the default 27 | /\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/, 28 | // include js/ts files 29 | "src/**/*.{js,ts}", 30 | ], 31 | }, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/components/CopyContainer.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 31 | -------------------------------------------------------------------------------- /src/components/ast/SummaryValue.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 34 | -------------------------------------------------------------------------------- /src/ui/navigation-menu/NavigationMenuList.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 36 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 27 | -------------------------------------------------------------------------------- /src/ui/select/SelectScrollUpButton.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 36 | -------------------------------------------------------------------------------- /src/ui/select/SelectScrollDownButton.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 36 | -------------------------------------------------------------------------------- /src/components/ui/Select.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 42 | -------------------------------------------------------------------------------- /src/components/sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | -------------------------------------------------------------------------------- /src/ui/input/Input.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 33 | -------------------------------------------------------------------------------- /src/ui/navigation-menu/NavigationMenu.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 41 | -------------------------------------------------------------------------------- /src/ui/navigation-menu/NavigationMenuTrigger.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 37 | -------------------------------------------------------------------------------- /src/ui/navigation-menu/index.ts: -------------------------------------------------------------------------------- 1 | import { cva } from "class-variance-authority"; 2 | 3 | export { default as NavigationMenu } from "./NavigationMenu.vue"; 4 | export { default as NavigationMenuContent } from "./NavigationMenuContent.vue"; 5 | export { default as NavigationMenuIndicator } from "./NavigationMenuIndicator.vue"; 6 | export { default as NavigationMenuItem } from "./NavigationMenuItem.vue"; 7 | export { default as NavigationMenuLink } from "./NavigationMenuLink.vue"; 8 | export { default as NavigationMenuList } from "./NavigationMenuList.vue"; 9 | export { default as NavigationMenuTrigger } from "./NavigationMenuTrigger.vue"; 10 | export { default as NavigationMenuViewport } from "./NavigationMenuViewport.vue"; 11 | 12 | export const navigationMenuTriggerStyle = cva( 13 | "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50", 14 | ); 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Boshen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ui/navigation-menu/NavigationMenuIndicator.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 36 | -------------------------------------------------------------------------------- /src/components/sidebar/Codegen.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 40 | -------------------------------------------------------------------------------- /src/ui/tabs/TabsTrigger.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 34 | -------------------------------------------------------------------------------- /src/components/sidebar/Define.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 40 | -------------------------------------------------------------------------------- /src/ui/radio-group/RadioGroupItem.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 40 | -------------------------------------------------------------------------------- /src/ui/navigation-menu/NavigationMenuViewport.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 36 | -------------------------------------------------------------------------------- /src/components/sidebar/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 35 | -------------------------------------------------------------------------------- /src/ui/navigation-menu/NavigationMenuContent.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 39 | -------------------------------------------------------------------------------- /src/ui/select/SelectTrigger.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 41 | -------------------------------------------------------------------------------- /src/ui/select/SelectItem.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 47 | -------------------------------------------------------------------------------- /src/components/output/EsTreePanel.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 43 | -------------------------------------------------------------------------------- /src/ui/checkbox/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 44 | -------------------------------------------------------------------------------- /src/components/header/Media.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 44 | -------------------------------------------------------------------------------- /src/components/sidebar/OptionsDialog.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 49 | -------------------------------------------------------------------------------- /src/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from "class-variance-authority"; 2 | 3 | export { default as Button } from "./Button.vue"; 4 | 5 | export const buttonVariants = cva( 6 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 7 | { 8 | variants: { 9 | variant: { 10 | default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", 11 | destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 12 | outline: 13 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 14 | secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 15 | ghost: "hover:bg-accent hover:text-accent-foreground", 16 | link: "text-primary underline-offset-4 hover:underline", 17 | }, 18 | size: { 19 | default: "h-9 px-4 py-2", 20 | xs: "h-7 rounded px-2", 21 | sm: "h-8 rounded-md px-3 text-xs", 22 | lg: "h-10 rounded-md px-8", 23 | icon: "h-9 w-9", 24 | }, 25 | }, 26 | defaultVariants: { 27 | variant: "default", 28 | size: "default", 29 | }, 30 | }, 31 | ); 32 | 33 | export type ButtonVariants = VariantProps; 34 | -------------------------------------------------------------------------------- /src/components/output/CodegenPanel.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 44 | -------------------------------------------------------------------------------- /src/components/input/InputEditor.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 52 | -------------------------------------------------------------------------------- /src/ui/switch/Switch.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 48 | -------------------------------------------------------------------------------- /src/components/sidebar/Minifier.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 51 | -------------------------------------------------------------------------------- /src/components/ast/Value.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 55 | -------------------------------------------------------------------------------- /src/utils/viz.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref } from "vue"; 2 | 3 | // Global viz instance - loaded lazily 4 | let vizInstance: any = null; 5 | const isLoading = ref(false); 6 | const isLoaded = ref(false); 7 | const loadError = ref(null); 8 | 9 | export async function createVizInstance() { 10 | if (vizInstance) return vizInstance; 11 | 12 | if (isLoading.value) { 13 | // Wait for existing load to complete 14 | return new Promise((resolve, reject) => { 15 | const checkLoaded = () => { 16 | if (vizInstance) { 17 | resolve(vizInstance); 18 | } else if (loadError.value) { 19 | reject(new Error(loadError.value)); 20 | } else { 21 | setTimeout(checkLoaded, 10); 22 | } 23 | }; 24 | checkLoaded(); 25 | }); 26 | } 27 | 28 | isLoading.value = true; 29 | loadError.value = null; 30 | 31 | try { 32 | const { instance } = await import("@viz-js/viz"); 33 | vizInstance = await instance(); 34 | isLoaded.value = true; 35 | return vizInstance; 36 | } catch (error) { 37 | const errorMessage = 38 | error instanceof Error ? error.message : "Failed to load visualization library"; 39 | loadError.value = errorMessage; 40 | console.warn("Failed to load @viz-js/viz:", error); 41 | throw new Error(errorMessage); 42 | } finally { 43 | isLoading.value = false; 44 | } 45 | } 46 | 47 | // Export reactive state for components 48 | export const vizLoading = computed(() => isLoading.value); 49 | export const vizLoaded = computed(() => isLoaded.value); 50 | export const vizError = computed(() => loadError.value); 51 | 52 | // Utility function to render SVG with error handling 53 | export async function renderSVG(dotSource: string): Promise { 54 | try { 55 | const viz = await createVizInstance(); 56 | return viz.renderSVGElement(dotSource); 57 | } catch (error) { 58 | console.warn("Failed to render graph:", error); 59 | return null; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/header/Switch.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 54 | -------------------------------------------------------------------------------- /src/components/output/OutputPreview.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@oxc-project/playground", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "packageManager": "pnpm@10.25.0", 7 | "description": "oxc playground", 8 | "license": "MIT", 9 | "homepage": "https://playground.oxc.rs", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/oxc-project/playground.git" 13 | }, 14 | "bugs": "https://github.com/oxc-project/playground/issues", 15 | "main": "", 16 | "engines": { 17 | "node": ">=20.14.0" 18 | }, 19 | "scripts": { 20 | "dev": "vite", 21 | "build": "vue-tsc && vite build", 22 | "preview": "vite preview", 23 | "fmt": "oxfmt", 24 | "typecheck": "vue-tsc", 25 | "lint": "eslint --cache", 26 | "prepare": "simple-git-hooks" 27 | }, 28 | "dependencies": { 29 | "@radix-icons/vue": "^1.0.0", 30 | "@unocss/reset": "^66.5.2", 31 | "@viz-js/viz": "^3.19.0", 32 | "@vueuse/core": "^14.0.0", 33 | "class-variance-authority": "^0.7.1", 34 | "clsx": "^2.1.1", 35 | "fflate": "^0.8.2", 36 | "monaco-editor": "0.55.1", 37 | "radix-vue": "^1.9.17", 38 | "tailwind-merge": "^3.3.1", 39 | "vue": "^3.5.22" 40 | }, 41 | "devDependencies": { 42 | "@emnapi/core": "^1.5.0", 43 | "@iconify-json/ri": "^1.2.5", 44 | "@sxzz/eslint-config": "^7.2.6", 45 | "@types/node": "^24.5.2", 46 | "@unocss/eslint-plugin": "^66.5.2", 47 | "@vitejs/plugin-vue": "^6.0.1", 48 | "eslint": "^9.36.0", 49 | "lint-staged": "^16.2.3", 50 | "oxc-playground": "link:../oxc/napi/playground", 51 | "oxfmt": "^0.19.0", 52 | "postcss": "^8.5.6", 53 | "shiki": "^3.13.0", 54 | "simple-git-hooks": "^2.13.1", 55 | "typescript": "^5.9.2", 56 | "unocss": "^66.5.2", 57 | "unocss-preset-animations": "^1.2.1", 58 | "unocss-preset-shadcn": "^1.0.1", 59 | "vite": "8.0.0-beta.1", 60 | "vue-tsc": "~3.1.0" 61 | }, 62 | "simple-git-hooks": { 63 | "pre-commit": "pnpm lint-staged" 64 | }, 65 | "lint-staged": { 66 | "*": "oxfmt --no-error-on-unmatched-pattern", 67 | "*.{js,jsx,tsx,ts,mts,css,md,json,yml,vue}": "eslint --fix --cache" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/components/sidebar/Transformer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 64 | -------------------------------------------------------------------------------- /src/ui/dialog/DialogContent.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 54 | -------------------------------------------------------------------------------- /src/ui/dialog/DialogScrollContent.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 67 | -------------------------------------------------------------------------------- /src/ui/select/SelectContent.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 64 | -------------------------------------------------------------------------------- /src/components/sidebar/Parser.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 73 | -------------------------------------------------------------------------------- /src/ui/splitter/SplitterResizeHandle.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 68 | -------------------------------------------------------------------------------- /src/components/output/OutputPanel.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 72 | -------------------------------------------------------------------------------- /src/components/IoContainer.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 67 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from "node:child_process"; 2 | import { existsSync, readFileSync } from "node:fs"; 3 | import path from "node:path"; 4 | import Vue from "@vitejs/plugin-vue"; 5 | import UnoCSS from "unocss/vite"; 6 | import { defineConfig } from "vite"; 7 | 8 | let oxcCommit: string | undefined; 9 | 10 | const COMMIT_FILE = "../oxc/napi/playground/git-commit"; 11 | if (existsSync(COMMIT_FILE)) { 12 | oxcCommit = readFileSync("../oxc/napi/playground/git-commit", "utf8").trim(); 13 | } 14 | 15 | if (!oxcCommit) { 16 | const { stdout } = spawnSync("git", ["rev-parse", "HEAD"], { 17 | cwd: "../oxc/napi/playground", 18 | encoding: "utf8", 19 | }); 20 | oxcCommit = stdout.trim(); 21 | } 22 | 23 | // https://vitejs.dev/config/ 24 | export default defineConfig({ 25 | resolve: { 26 | alias: { 27 | "~": path.resolve(__dirname, "./src"), 28 | }, 29 | }, 30 | define: { 31 | "import.meta.env.OXC_COMMIT": JSON.stringify(oxcCommit), 32 | }, 33 | build: { 34 | target: "esnext", 35 | rollupOptions: { 36 | output: { 37 | manualChunks: (id) => { 38 | // Vue ecosystem 39 | if (id.includes("vue") || id.includes("@vueuse")) { 40 | return "vue-vendor"; 41 | } 42 | 43 | // UI libraries 44 | if ( 45 | id.includes("radix-vue") || 46 | id.includes("class-variance-authority") || 47 | id.includes("clsx") || 48 | id.includes("tailwind-merge") || 49 | id.includes("@radix-icons") 50 | ) { 51 | return "ui-vendor"; 52 | } 53 | 54 | // Monaco Editor core 55 | if (id.includes("monaco-editor/esm/vs/editor/editor.api")) { 56 | return "monaco-vendor"; 57 | } 58 | 59 | // Utility libraries 60 | if (id.includes("fflate")) { 61 | return "utils-vendor"; 62 | } 63 | 64 | // Keep dynamic imports as separate chunks (Shiki, Viz) 65 | // They will be handled automatically by Vite 66 | }, 67 | }, 68 | }, 69 | }, 70 | experimental: { 71 | enableNativePlugin: true, 72 | }, 73 | server: { 74 | // These two cross origin headers are used to fix the following error: 75 | // TypeError: Failed to execute 'decode' on 'TextDecoder': The provided ArrayBufferView value must not be shared. 76 | // The same headers are added to netlify via `dist/_headers` 77 | headers: { 78 | "Cross-Origin-Opener-Policy": "same-origin", 79 | "Cross-Origin-Embedder-Policy": "require-corp", 80 | }, 81 | fs: { 82 | allow: [__dirname, "../oxc/napi/playground"], 83 | }, 84 | }, 85 | plugins: [Vue(), UnoCSS()], 86 | }); 87 | -------------------------------------------------------------------------------- /src/components/header/Nav.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 90 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 */3 * * *" # Build and deploy every 3 hours. 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | paths-ignore: 12 | - README.md 13 | push: 14 | branches: 15 | - main 16 | paths-ignore: 17 | - README.md 18 | 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} 21 | cancel-in-progress: ${{ github.ref_name != 'main' }} 22 | 23 | permissions: {} 24 | 25 | jobs: 26 | check-typos: 27 | name: Check typos 28 | if: github.event_name != 'schedule' 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 32 | - uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06 # v1.40.0 33 | with: 34 | files: . 35 | 36 | build: 37 | name: Build 38 | runs-on: ubuntu-latest 39 | env: 40 | SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 41 | permissions: 42 | pull-requests: write 43 | contents: write 44 | steps: 45 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 46 | with: 47 | repository: oxc-project/oxc 48 | ref: main 49 | persist-credentials: false 50 | 51 | - uses: oxc-project/setup-node@141eb77546de6702f92d320926403fe3f9f6a6f2 # v1.0.5 52 | 53 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11 54 | with: 55 | save-cache: ${{ github.ref_name == 'main' }} 56 | tools: just 57 | 58 | - run: rustup target add wasm32-wasip1-threads 59 | 60 | - run: just build-playground 61 | - run: git rev-parse HEAD > napi/playground/git-commit 62 | 63 | - run: cd .. && mv playground oxc && mkdir playground 64 | 65 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 66 | 67 | - uses: oxc-project/setup-node@141eb77546de6702f92d320926403fe3f9f6a6f2 # v1.0.5 68 | 69 | - run: pnpm run build 70 | 71 | - name: Install Netlify 72 | if: env.SITE_ID 73 | run: pnpm install -g netlify-cli@18.1.0 74 | 75 | # https://www.raulmelo.me/en/blog/deploying-netlify-github-actions-guide 76 | - name: Deploy to Netlify 77 | id: netlify_deploy 78 | if: env.SITE_ID 79 | run: | 80 | mv _headers dist 81 | prod_flag="" 82 | if [ "${GITHUB_REF_NAME}" = "main" ]; then prod_flag="--prod"; fi 83 | echo $prod_flag 84 | netlify deploy \ 85 | --dir dist \ 86 | --site ${{ secrets.NETLIFY_SITE_ID }} \ 87 | --auth ${{ secrets.NETLIFY_API_TOKEN }} \ 88 | $prod_flag \ 89 | --json \ 90 | > deploy_output.json 91 | cat deploy_output.json 92 | 93 | - name: Generate URL Preview 94 | id: url_preview 95 | if: env.SITE_ID && github.event_name == 'pull_request' 96 | run: | 97 | NETLIFY_PREVIEW_URL=$(jq -r '.deploy_url' deploy_output.json) 98 | echo "NETLIFY_PREVIEW_URL=$NETLIFY_PREVIEW_URL" >> $GITHUB_OUTPUT 99 | 100 | - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 101 | if: env.SITE_ID && github.event_name == 'pull_request' 102 | with: 103 | token: ${{ secrets.GITHUB_TOKEN }} 104 | issue-number: ${{ github.event.number }} 105 | body: "Preview URL: ${{ steps.url_preview.outputs.NETLIFY_PREVIEW_URL }}" 106 | -------------------------------------------------------------------------------- /src/components/output/ControlflowPanel.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 101 | -------------------------------------------------------------------------------- /src/components/output/FormatterPanel.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 116 | 117 | 130 | -------------------------------------------------------------------------------- /src/components/MonacoEditor.vue: -------------------------------------------------------------------------------- 1 | 150 | 151 | 154 | -------------------------------------------------------------------------------- /src/components/ast/Property.vue: -------------------------------------------------------------------------------- 1 | 108 | 109 | 144 | -------------------------------------------------------------------------------- /src/utils/shiki.ts: -------------------------------------------------------------------------------- 1 | import { useMemoize } from "@vueuse/core"; 2 | import { computed, ref, toValue, watchEffect, type MaybeRefOrGetter } from "vue"; 3 | import { dark } from "~/composables/state"; 4 | import type { HighlighterCore } from "shiki/core"; 5 | 6 | // Global highlighter instance - loaded lazily 7 | let highlighterInstance: HighlighterCore | null = null; 8 | const isLoading = ref(false); 9 | const isLoaded = ref(false); 10 | 11 | export async function createHighlighter(): Promise { 12 | if (highlighterInstance) return highlighterInstance; 13 | 14 | if (isLoading.value) { 15 | // Wait for existing load to complete 16 | return new Promise((resolve) => { 17 | const checkLoaded = () => { 18 | if (highlighterInstance) { 19 | resolve(highlighterInstance); 20 | } else { 21 | setTimeout(checkLoaded, 10); 22 | } 23 | }; 24 | checkLoaded(); 25 | }); 26 | } 27 | 28 | isLoading.value = true; 29 | 30 | try { 31 | const [ 32 | { createHighlighterCore }, 33 | { createJavaScriptRegexEngine }, 34 | langJson, 35 | langTs, 36 | langTsx, 37 | vitesseLight, 38 | vitesseDark, 39 | ] = await Promise.all([ 40 | import("shiki/core"), 41 | import("shiki/engine/javascript"), 42 | import("shiki/langs/json.mjs"), 43 | import("shiki/langs/typescript.mjs"), 44 | import("shiki/langs/tsx.mjs"), 45 | import("shiki/themes/vitesse-light.mjs"), 46 | import("shiki/themes/vitesse-dark.mjs"), 47 | ]); 48 | 49 | highlighterInstance = await createHighlighterCore({ 50 | themes: [vitesseLight.default, vitesseDark.default], 51 | langs: [langJson.default, langTs.default, langTsx.default], 52 | engine: createJavaScriptRegexEngine(), 53 | }); 54 | 55 | isLoaded.value = true; 56 | return highlighterInstance; 57 | } finally { 58 | isLoading.value = false; 59 | } 60 | } 61 | 62 | // Export reactive state for components to check loading status 63 | export const shikiLoading = computed(() => isLoading.value); 64 | export const shikiLoaded = computed(() => isLoaded.value); 65 | 66 | export type ShikiLang = "json" | "tsx" | "text" | "typescript"; 67 | 68 | export async function highlight(code: string, lang: ShikiLang): Promise { 69 | try { 70 | const highlighter = await createHighlighter(); 71 | return highlighter.codeToHtml(code, { 72 | lang, 73 | theme: dark.value ? "vitesse-dark" : "vitesse-light", 74 | transformers: [ 75 | { 76 | name: "add-style", 77 | pre(node) { 78 | this.addClassToHast(node, "!bg-transparent p-2"); 79 | }, 80 | }, 81 | ], 82 | }); 83 | } catch (error) { 84 | console.warn("Failed to load Shiki highlighter:", error); 85 | // Fallback to plain text with basic styling 86 | return `
${escapeHtml(code)}
`; 87 | } 88 | } 89 | 90 | function escapeHtml(text: string): string { 91 | const div = document.createElement("div"); 92 | div.textContent = text; 93 | return div.innerHTML; 94 | } 95 | 96 | const highlightToken = useMemoize(async (code: string, theme: string) => { 97 | try { 98 | const highlighter = await createHighlighter(); 99 | return highlighter.codeToTokens(code, { 100 | lang: "typescript", 101 | theme, 102 | }); 103 | } catch { 104 | return { tokens: [[{ color: "#666666" }]] }; // Fallback gray color 105 | } 106 | }); 107 | 108 | export function useHighlightColor(content: MaybeRefOrGetter) { 109 | const color = ref("#666666"); // Default color 110 | 111 | // Reactive computation that updates color when content or theme changes 112 | watchEffect(async () => { 113 | const code = toValue(content); 114 | if (code == null) { 115 | color.value = ""; 116 | return; 117 | } 118 | 119 | try { 120 | const theme = `vitesse-${dark.value ? "dark" : "light"}`; 121 | // process the highlight after main rendering completes to prevent laggy 122 | await new Promise((res) => requestIdleCallback(res, { timeout: 500 })); 123 | const result = await highlightToken(code, theme); 124 | const token = result.tokens[0]; 125 | const idx = code.startsWith('"') && token.length > 1 ? 1 : 0; 126 | color.value = token[idx].color || "#666666"; 127 | } catch { 128 | color.value = "#666666"; // Fallback color 129 | } 130 | }); 131 | 132 | return computed(() => color.value); 133 | } 134 | -------------------------------------------------------------------------------- /src/composables/oxc.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalState, watchDebounced } from "@vueuse/core"; 2 | import { computed, ref, shallowRef, toRaw, triggerRef, watch } from "vue"; 3 | import { activeTab, editorValue } from "~/composables/state"; 4 | import { PLAYGROUND_DEMO_CODE } from "~/utils/constants"; 5 | import { atou, utoa } from "~/utils/url"; 6 | import type { Oxc, OxcOptions } from "oxc-playground"; 7 | 8 | async function initialize(): Promise { 9 | const { Oxc } = await import("oxc-playground"); 10 | return new Oxc(); 11 | } 12 | 13 | export const loadingOxc = ref(true); 14 | export const oxcPromise = initialize().finally(() => (loadingOxc.value = false)); 15 | 16 | export const useOxc = createGlobalState(async () => { 17 | const options = ref>({ 18 | run: { 19 | lint: true, 20 | formatter: false, 21 | transform: false, 22 | isolatedDeclarations: false, 23 | whitespace: false, 24 | mangle: false, 25 | compress: false, 26 | scope: true, 27 | symbol: true, 28 | cfg: true, 29 | }, 30 | parser: { 31 | extension: "tsx", 32 | allowReturnOutsideFunction: true, 33 | preserveParens: true, 34 | allowV8Intrinsics: true, 35 | semanticErrors: true, 36 | }, 37 | linter: {}, 38 | formatter: { 39 | useTabs: false, 40 | tabWidth: 2, 41 | endOfLine: "lf", 42 | printWidth: 80, 43 | singleQuote: false, 44 | jsxSingleQuote: false, 45 | quoteProps: "as-needed", 46 | trailingComma: "all", 47 | semi: true, 48 | arrowParens: "always", 49 | bracketSpacing: true, 50 | bracketSameLine: false, 51 | objectWrap: "preserve", 52 | singleAttributePerLine: false, 53 | experimentalSortImports: undefined, 54 | }, 55 | transformer: { 56 | target: "es2015", 57 | useDefineForClassFields: true, 58 | experimentalDecorators: true, 59 | emitDecoratorMetadata: true, 60 | }, 61 | isolatedDeclarations: { 62 | stripInternal: false, 63 | }, 64 | codegen: { 65 | normal: true, 66 | jsdoc: true, 67 | annotation: true, 68 | legal: true, 69 | }, 70 | compress: {}, 71 | mangle: { 72 | topLevel: true, 73 | keepNames: false, 74 | }, 75 | controlFlow: { 76 | verbose: false, 77 | }, 78 | inject: { inject: {} }, 79 | define: { define: {} }, 80 | }); 81 | const oxc = await oxcPromise; 82 | const state = shallowRef(oxc); 83 | const error = ref(); 84 | 85 | function run() { 86 | const errors: unknown[] = []; 87 | const originalError = console.error; 88 | console.error = function (...msgs) { 89 | errors.push(...msgs); 90 | return originalError.apply(this, msgs); 91 | }; 92 | try { 93 | const rawOptions = toRaw(options.value); 94 | if (typeof rawOptions?.linter?.config === "object") { 95 | rawOptions.linter.config = JSON.stringify(rawOptions.linter.config); 96 | } 97 | oxc.run(editorValue.value, rawOptions); 98 | // Reset error if successful 99 | error.value = undefined; 100 | } catch (error_) { 101 | console.error(error_); 102 | error.value = errors.length ? errors : error_; 103 | } 104 | console.error = originalError; 105 | triggerRef(state); 106 | } 107 | watch([options, editorValue, activeTab], run, { deep: true }); 108 | 109 | let rawUrlState: string | undefined; 110 | let urlState: any; 111 | try { 112 | rawUrlState = atou(location.hash!.slice(1)); 113 | urlState = rawUrlState && JSON.parse(rawUrlState); 114 | } catch (error) { 115 | console.error(error); 116 | } 117 | 118 | if (urlState?.o) { 119 | options.value = urlState.o; 120 | } 121 | 122 | if (urlState?.t) { 123 | activeTab.value = urlState.t; 124 | } 125 | 126 | editorValue.value = urlState?.c ?? PLAYGROUND_DEMO_CODE; 127 | 128 | watchDebounced( 129 | () => [editorValue.value, options.value, activeTab.value], 130 | ([editorValue, options, activeTab]) => { 131 | const serialized = JSON.stringify({ 132 | c: editorValue === PLAYGROUND_DEMO_CODE ? "" : editorValue, 133 | o: options, 134 | t: activeTab, 135 | }); 136 | 137 | try { 138 | history.replaceState({}, "", `#${utoa(serialized)}`); 139 | } catch (error) { 140 | console.error(error); 141 | } 142 | }, 143 | { debounce: 2000 }, 144 | ); 145 | 146 | const monacoLanguage = computed(() => { 147 | const filename = `test.${options.value.parser.extension}`; 148 | const ext = filename.split(".").pop()!; 149 | if (["ts", "mts", "cts", "tsx"].includes(ext)) return "typescript"; 150 | if (["js", "mjs", "cjs", "jsx"].includes(ext)) return "javascript"; 151 | return "plaintext"; 152 | }); 153 | 154 | // NOTE: do not free() on unmount. that hook is fired any time any consuming 155 | // component unmounts, which messes things up for other components. 156 | 157 | return { 158 | oxc: state, 159 | error, 160 | options, 161 | monacoLanguage, 162 | }; 163 | }); 164 | --------------------------------------------------------------------------------