├── _redirects ├── src ├── components │ ├── common │ │ ├── Logo │ │ │ ├── index.ts │ │ │ ├── Logo.stories.tsx │ │ │ └── Logo.tsx │ │ ├── Monaco │ │ │ ├── index.ts │ │ │ ├── themes │ │ │ │ ├── vs-light.json │ │ │ │ └── vs-dark.json │ │ │ ├── Monaco.stories.tsx │ │ │ └── Monaco.tsx │ │ ├── FileTree │ │ │ ├── index.ts │ │ │ └── FileTree.stories.tsx │ │ ├── MonacoTab │ │ │ ├── index.ts │ │ │ ├── MonacoTab.stories.tsx │ │ │ ├── TabContextMenu.tsx │ │ │ └── MonacoTab.tsx │ │ └── MetricCard │ │ │ ├── index.ts │ │ │ ├── MetricCard.stories.tsx │ │ │ └── MetricCard.tsx │ ├── layout │ │ ├── Header │ │ │ ├── index.ts │ │ │ ├── Header.stories.tsx │ │ │ └── Header.tsx │ │ └── ThemeSwitcher.tsx │ ├── ExportModal │ │ ├── index.ts │ │ ├── ExportModal.stories.tsx │ │ └── ExportModal.tsx │ ├── playground │ │ ├── Sidebar │ │ │ ├── index.ts │ │ │ ├── Sidebar.stories.tsx │ │ │ └── Sidebar.tsx │ │ ├── ShareDialog │ │ │ ├── index.ts │ │ │ └── ShareDialog.stories.tsx │ │ ├── SidebarIcon │ │ │ ├── index.ts │ │ │ ├── SidebarIcon.stories.tsx │ │ │ └── SidebarIcon.tsx │ │ ├── code │ │ │ └── RunPanel │ │ │ │ ├── index.ts │ │ │ │ ├── tabs │ │ │ │ ├── RunTab │ │ │ │ │ ├── index.ts │ │ │ │ │ └── RunTab.stories.tsx │ │ │ │ └── ConsoleTab │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ConsoleTab.stories.tsx │ │ │ │ │ └── ConsoleTab.tsx │ │ │ │ ├── RunPanel.stories.tsx │ │ │ │ └── RunPanel.tsx │ │ └── compare │ │ │ ├── ComparisonChart │ │ │ ├── index.ts │ │ │ ├── ComparisonChart.tsx │ │ │ └── ComparisonChart.stories.tsx │ │ │ └── ComparisonTable │ │ │ ├── index.ts │ │ │ ├── ComparisonTable.stories.tsx │ │ │ └── ComparisonTable.tsx │ └── ui │ │ ├── label.tsx │ │ ├── progress.tsx │ │ ├── input.tsx │ │ ├── checkbox.tsx │ │ ├── switch.tsx │ │ ├── tooltip.tsx │ │ ├── badge.tsx │ │ ├── scroll-area.tsx │ │ ├── resizable.tsx │ │ ├── card.tsx │ │ ├── tabs.tsx │ │ ├── button.tsx │ │ ├── table.tsx │ │ ├── dialog.tsx │ │ ├── select.tsx │ │ ├── context-menu.tsx │ │ └── dropdown-menu.tsx ├── services │ ├── dependencies │ │ ├── index.ts │ │ ├── cachedFetch.ts │ │ ├── ata.ts │ │ ├── cache.ts │ │ ├── DependencyService.ts │ │ └── npmSearch.ts │ ├── code-processor │ │ ├── index.ts │ │ ├── prettier.ts │ │ ├── babel.ts │ │ ├── babel.test.ts │ │ ├── bundle-benchmark-code.ts │ │ └── bundle-benchmark-code.test.ts │ └── benchmark │ │ ├── performance.d.ts │ │ ├── types.ts │ │ └── worker.ts ├── config.ts ├── lib │ ├── utils.ts │ └── formatters.ts ├── routes.ts ├── stores │ ├── userStore.ts │ ├── dependenciesStore.ts │ ├── persistentStore.ts │ └── benchmarkStore.ts ├── constants.ts ├── global.css ├── root.tsx ├── routes │ └── playground │ │ ├── root.tsx │ │ └── views │ │ └── compare │ │ └── index.tsx └── hooks │ └── useMonacoTabs.ts ├── .dockerignore ├── netlify.toml ├── public └── favicon.ico ├── .gitignore ├── react-router.config.ts ├── vitest.setup.ts ├── .storybook ├── preview.ts └── main.ts ├── components.json ├── tsconfig.json ├── Dockerfile ├── vitest.config.ts ├── Dockerfile.bun ├── Dockerfile.pnpm ├── vite.config.ts ├── README.md ├── tailwind.config.ts └── package.json /_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /src/components/common/Logo/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Logo"; 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .react-router 2 | build 3 | node_modules 4 | README.md -------------------------------------------------------------------------------- /src/components/common/Monaco/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Monaco"; 2 | -------------------------------------------------------------------------------- /src/components/layout/Header/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Header"; 2 | -------------------------------------------------------------------------------- /src/components/ExportModal/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ExportModal"; 2 | -------------------------------------------------------------------------------- /src/components/common/FileTree/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./FileTree"; 2 | -------------------------------------------------------------------------------- /src/components/common/MonacoTab/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MonacoTab"; 2 | -------------------------------------------------------------------------------- /src/components/playground/Sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Sidebar"; 2 | -------------------------------------------------------------------------------- /src/components/common/MetricCard/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MetricCard"; 2 | -------------------------------------------------------------------------------- /src/services/dependencies/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./DependencyService"; 2 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | to = "/" 4 | status = 200 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rd/benchjs/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/components/playground/ShareDialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ShareDialog"; 2 | -------------------------------------------------------------------------------- /src/components/playground/SidebarIcon/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SidebarIcon"; 2 | -------------------------------------------------------------------------------- /src/components/playground/code/RunPanel/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RunPanel"; 2 | -------------------------------------------------------------------------------- /src/components/playground/code/RunPanel/tabs/RunTab/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RunTab"; 2 | -------------------------------------------------------------------------------- /src/components/playground/code/RunPanel/tabs/ConsoleTab/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ConsoleTab"; 2 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const features = { 2 | memory: { 3 | enabled: false, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/playground/compare/ComparisonChart/index.ts: -------------------------------------------------------------------------------- 1 | export { ComparisonChart } from "./ComparisonChart"; 2 | -------------------------------------------------------------------------------- /src/components/playground/compare/ComparisonTable/index.ts: -------------------------------------------------------------------------------- 1 | export { ComparisonTable } from "./ComparisonTable"; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | 4 | # React Router 5 | /.react-router/ 6 | /build/ 7 | /public/monaco-editor/ 8 | 9 | *storybook.log 10 | -------------------------------------------------------------------------------- /src/components/common/Monaco/themes/vs-light.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "VS Light", 3 | "base": "vs", 4 | "inherit": true, 5 | "rules": [], 6 | "colors": {} 7 | } 8 | -------------------------------------------------------------------------------- /src/components/common/Monaco/themes/vs-dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "VS Dark", 3 | "base": "vs-dark", 4 | "inherit": true, 5 | "rules": [], 6 | "colors": {} 7 | } 8 | -------------------------------------------------------------------------------- /react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@react-router/dev/config"; 2 | 3 | export default { 4 | appDirectory: "src", 5 | ssr: false, 6 | } satisfies Config; 7 | -------------------------------------------------------------------------------- /src/services/code-processor/index.ts: -------------------------------------------------------------------------------- 1 | import { bundleBenchmarkCode } from "./bundle-benchmark-code"; 2 | 3 | export const codeProcessor = { 4 | bundleBenchmarkCode, 5 | }; 6 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export const cn = (...inputs: ClassValue[]) => { 5 | return twMerge(clsx(inputs)); 6 | }; 7 | -------------------------------------------------------------------------------- /src/routes.ts: -------------------------------------------------------------------------------- 1 | import { index, route, type RouteConfig } from "@react-router/dev/routes"; 2 | 3 | export default [ 4 | // 5 | index("routes/home.tsx"), 6 | route("playground", "routes/playground/root.tsx"), 7 | ] satisfies RouteConfig; 8 | -------------------------------------------------------------------------------- /vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { afterEach } from "vitest"; 2 | 3 | afterEach(() => { 4 | vi.clearAllTimers(); 5 | vi.useRealTimers(); 6 | }); 7 | 8 | afterAll(() => { 9 | vi.unstubAllGlobals(); 10 | vi.clearAllMocks(); 11 | }); 12 | -------------------------------------------------------------------------------- /src/services/benchmark/performance.d.ts: -------------------------------------------------------------------------------- 1 | interface Performance { 2 | measureUserAgentSpecificMemory: () => Promise<{ 3 | bytes: number; 4 | breakdown: { 5 | bytes: number; 6 | attribution: string[]; 7 | types: string[]; 8 | }[]; 9 | }>; 10 | } 11 | -------------------------------------------------------------------------------- /src/services/code-processor/prettier.ts: -------------------------------------------------------------------------------- 1 | import parserBabel from "prettier/plugins/babel"; 2 | import parserEstree from "prettier/plugins/estree"; 3 | import { format as prettierFormat } from "prettier/standalone"; 4 | 5 | export const format = (code: string) => { 6 | return prettierFormat(code.trim(), { 7 | parser: "babel", 8 | semi: true, 9 | singleQuote: false, 10 | plugins: [parserBabel, parserEstree], 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | import { withRouter, reactRouterParameters } from "storybook-addon-remix-react-router"; 3 | import "../src/global.css"; 4 | 5 | const preview: Preview = { 6 | parameters: { 7 | controls: { 8 | matchers: { 9 | color: /(background|color)$/i, 10 | date: /Date$/i, 11 | }, 12 | }, 13 | reactRouter: reactRouterParameters({}), 14 | }, 15 | decorators: [withRouter], 16 | }; 17 | 18 | export default preview; 19 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/global.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /src/components/playground/Sidebar/Sidebar.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Sidebar } from "./Sidebar"; 3 | 4 | const meta: Meta = { 5 | title: "Playground/Sidebar", 6 | component: Sidebar, 7 | render: (args) => ( 8 |
9 | 10 |
11 | ), 12 | }; 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = { 18 | args: {}, 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/common/MetricCard/MetricCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { ClockIcon } from "lucide-react"; 3 | import { MetricCard } from "./MetricCard"; 4 | 5 | const meta: Meta = { 6 | title: "Common/MetricCard", 7 | component: MetricCard, 8 | }; 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Default: Story = { 14 | args: { 15 | title: "Elapsed Time", 16 | value: "0ms", 17 | icon: ClockIcon, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | 3 | const config: StorybookConfig = { 4 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 5 | addons: [ 6 | "@storybook/addon-essentials", 7 | "@storybook/addon-interactions", 8 | // https://github.com/JesusTheHun/storybook-addon-remix-react-router 9 | "storybook-addon-remix-react-router", 10 | ], 11 | framework: { 12 | name: "@storybook/react-vite", 13 | options: {}, 14 | }, 15 | }; 16 | export default config; 17 | -------------------------------------------------------------------------------- /src/services/dependencies/cachedFetch.ts: -------------------------------------------------------------------------------- 1 | import { cache } from "./cache"; 2 | 3 | export const cachedFetch = async (url: RequestInfo | URL, opts?: RequestInit) => { 4 | const path = url.toString(); 5 | const cached = await cache.get(path); 6 | if (cached) { 7 | return new Response(cached, { headers: { "Content-Type": "text/javascript" } }); 8 | } 9 | const response = await fetch(url, opts); 10 | const clone = response.clone(); 11 | const content = await clone.text(); 12 | await cache.set(path, content); 13 | return response; 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/playground/code/RunPanel/RunPanel.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { RunPanel } from "./RunPanel"; 3 | 4 | const meta: Meta = { 5 | title: "Playground/Code/RunPanel", 6 | component: RunPanel, 7 | }; 8 | export default meta; 9 | 10 | type Story = StoryObj; 11 | 12 | export const Default: Story = { 13 | args: { 14 | implementation: { 15 | id: "main.ts", 16 | filename: "main.ts", 17 | content: "// Write your implementation here\n", 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/common/Logo/Logo.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Logo } from "./Logo"; 3 | 4 | const meta: Meta = { 5 | title: "Common/Logo", 6 | component: Logo, 7 | }; 8 | export default meta; 9 | 10 | type Story = StoryObj; 11 | 12 | export const Default: Story = { 13 | args: {}, 14 | }; 15 | 16 | export const Large: Story = { 17 | args: { 18 | size: "large", 19 | }, 20 | }; 21 | 22 | export const Huge: Story = { 23 | args: { 24 | size: "huge", 25 | }, 26 | }; 27 | 28 | export const NoIcon: Story = { 29 | args: { 30 | noIcon: true, 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/layout/Header/Header.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Header } from "./Header"; 4 | 5 | const meta: Meta = { 6 | title: "Layout/Header", 7 | component: Header, 8 | }; 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Default: Story = { 14 | args: {}, 15 | }; 16 | 17 | export const CustomNav: Story = { 18 | args: { 19 | customNav: ( 20 | 23 | ), 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM", "DOM.Iterable", "ES2022", "WebWorker"], 4 | "target": "ES2022", 5 | "module": "ES2022", 6 | "moduleResolution": "bundler", 7 | "jsx": "react-jsx", 8 | "esModuleInterop": true, 9 | "noEmit": true, 10 | "resolveJsonModule": true, 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "rootDirs": [".", "./.react-router/types"], 14 | "baseUrl": ".", 15 | "paths": { 16 | "@/*": ["./src/*"] 17 | }, 18 | "types": ["vite/client", "vitest/globals", "vitest/config"] 19 | }, 20 | "include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"] 21 | } 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS development-dependencies-env 2 | COPY . /app 3 | WORKDIR /app 4 | RUN npm ci 5 | 6 | FROM node:20-alpine AS production-dependencies-env 7 | COPY ./package.json package-lock.json /app/ 8 | WORKDIR /app 9 | RUN npm ci --omit=dev 10 | 11 | FROM node:20-alpine AS build-env 12 | COPY . /app/ 13 | COPY --from=development-dependencies-env /app/node_modules /app/node_modules 14 | WORKDIR /app 15 | RUN npm run build 16 | 17 | FROM node:20-alpine 18 | COPY ./package.json package-lock.json /app/ 19 | COPY --from=production-dependencies-env /app/node_modules /app/node_modules 20 | COPY --from=build-env /app/build /app/build 21 | WORKDIR /app 22 | CMD ["npm", "run", "start"] 23 | -------------------------------------------------------------------------------- /src/components/playground/SidebarIcon/SidebarIcon.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Search } from "lucide-react"; 3 | import { SidebarIcon } from "./SidebarIcon"; 4 | 5 | const meta: Meta = { 6 | title: "Playground/SidebarIcon", 7 | component: SidebarIcon, 8 | render: (args) => ( 9 |
10 | 11 |
12 | ), 13 | }; 14 | export default meta; 15 | 16 | type Story = StoryObj; 17 | 18 | export const Default: Story = { 19 | args: { 20 | icon: Search, 21 | isActive: false, 22 | tooltip: "Search", 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/services/dependencies/ata.ts: -------------------------------------------------------------------------------- 1 | import { ATABootstrapConfig, setupTypeAcquisition } from "@typescript/ata"; 2 | import ts from "typescript"; 3 | 4 | type CreateATAOptions = { 5 | fetcher: ATABootstrapConfig["fetcher"]; 6 | handlers: Partial; 7 | }; 8 | 9 | export const createATA = (opts: CreateATAOptions) => 10 | setupTypeAcquisition({ 11 | projectName: "BenchJS", 12 | typescript: ts, 13 | logger: console, 14 | delegate: { 15 | receivedFile: opts.handlers.receivedFile, 16 | started: opts.handlers.started, 17 | progress: opts.handlers.progress, 18 | finished: opts.handlers.finished, 19 | errorMessage: opts.handlers.errorMessage, 20 | }, 21 | fetcher: opts.fetcher, 22 | }); 23 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | includeSource: ["src/**/*.{ts,tsx}"], 8 | environment: "node", 9 | setupFiles: ["node:timers/promises", "vitest.setup.ts"], 10 | coverage: { 11 | include: ["src/**/*.{ts,tsx}"], 12 | exclude: ["**/*.test.{ts,tsx}"], 13 | reporter: process.env.CI ? "json" : "html-spa", 14 | thresholds: { 15 | statements: 90, 16 | functions: 90, 17 | branches: 90, 18 | lines: 90, 19 | }, 20 | }, 21 | testTimeout: 5000, 22 | }, 23 | resolve: { 24 | alias: { 25 | "@": path.resolve(__dirname, "./src"), 26 | }, 27 | }, 28 | }); 29 | 30 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | const labelVariants = cva( 7 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 8 | ); 9 | 10 | const Label = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef & VariantProps 13 | >(({ className, ...props }, ref) => ( 14 | 15 | )); 16 | Label.displayName = LabelPrimitive.Root.displayName; 17 | 18 | export { Label }; 19 | -------------------------------------------------------------------------------- /Dockerfile.bun: -------------------------------------------------------------------------------- 1 | FROM oven/bun:1 AS dependencies-env 2 | COPY . /app 3 | 4 | FROM dependencies-env AS development-dependencies-env 5 | COPY ./package.json bun.lockb /app/ 6 | WORKDIR /app 7 | RUN bun i --frozen-lockfile 8 | 9 | FROM dependencies-env AS production-dependencies-env 10 | COPY ./package.json bun.lockb /app/ 11 | WORKDIR /app 12 | RUN bun i --production 13 | 14 | FROM dependencies-env AS build-env 15 | COPY ./package.json bun.lockb /app/ 16 | COPY --from=development-dependencies-env /app/node_modules /app/node_modules 17 | WORKDIR /app 18 | RUN bun run build 19 | 20 | FROM dependencies-env 21 | COPY ./package.json bun.lockb /app/ 22 | COPY --from=production-dependencies-env /app/node_modules /app/node_modules 23 | COPY --from=build-env /app/build /app/build 24 | WORKDIR /app 25 | CMD ["bun", "run", "start"] 26 | -------------------------------------------------------------------------------- /src/components/common/MetricCard/MetricCard.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent } from "@/components/ui/card"; 2 | 3 | interface MetricCardProps { 4 | title: string; 5 | value: string; 6 | icon?: React.ElementType; 7 | } 8 | 9 | export const MetricCard = ({ title, value, icon: Icon }: MetricCardProps) => { 10 | return ( 11 | 12 | 13 | {Icon && ( 14 |
15 | 16 |
17 | )} 18 |
19 |

{title}

20 |

{value}

21 |
22 |
23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/common/Monaco/Monaco.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Monaco } from "./Monaco"; 3 | 4 | const meta: Meta = { 5 | title: "Common/Monaco", 6 | component: Monaco, 7 | render: (args) => ( 8 |
9 | 10 |
11 | ), 12 | }; 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = { 18 | args: {}, 19 | }; 20 | 21 | export const WithTabs: Story = { 22 | args: { 23 | tabs: [ 24 | { 25 | id: "main.ts", 26 | name: "main.ts", 27 | active: true, 28 | }, 29 | { 30 | id: "setup.ts", 31 | name: "setup.ts", 32 | active: false, 33 | }, 34 | ], 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /Dockerfile.pnpm: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS dependencies-env 2 | RUN npm i -g pnpm 3 | COPY . /app 4 | 5 | FROM dependencies-env AS development-dependencies-env 6 | COPY ./package.json pnpm-lock.yaml /app/ 7 | WORKDIR /app 8 | RUN pnpm i --frozen-lockfile 9 | 10 | FROM dependencies-env AS production-dependencies-env 11 | COPY ./package.json pnpm-lock.yaml /app/ 12 | WORKDIR /app 13 | RUN pnpm i --prod --frozen-lockfile 14 | 15 | FROM dependencies-env AS build-env 16 | COPY ./package.json pnpm-lock.yaml /app/ 17 | COPY --from=development-dependencies-env /app/node_modules /app/node_modules 18 | WORKDIR /app 19 | RUN pnpm build 20 | 21 | FROM dependencies-env 22 | COPY ./package.json pnpm-lock.yaml /app/ 23 | COPY --from=production-dependencies-env /app/node_modules /app/node_modules 24 | COPY --from=build-env /app/build /app/build 25 | WORKDIR /app 26 | CMD ["pnpm", "start"] 27 | -------------------------------------------------------------------------------- /src/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ProgressPrimitive from "@radix-ui/react-progress"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Progress = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, value, ...props }, ref) => ( 10 | 15 | 19 | 20 | )); 21 | Progress.displayName = ProgressPrimitive.Root.displayName; 22 | 23 | export { Progress }; 24 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | const Input = React.forwardRef>( 5 | ({ className, type, ...props }, ref) => { 6 | return ( 7 | 16 | ); 17 | }, 18 | ); 19 | Input.displayName = "Input"; 20 | 21 | export { Input }; 22 | -------------------------------------------------------------------------------- /src/services/dependencies/cache.ts: -------------------------------------------------------------------------------- 1 | import { openDB } from "idb"; 2 | 3 | const DB_NAME = "dependencies-cache"; 4 | const STORE_NAME = "files"; 5 | const VERSION = 1; 6 | 7 | const getDB = () => { 8 | if (typeof window === "undefined") return null; 9 | return openDB(DB_NAME, VERSION, { 10 | upgrade(db) { 11 | db.createObjectStore(STORE_NAME); 12 | }, 13 | }); 14 | }; 15 | 16 | export const cache = { 17 | async get(key: string) { 18 | const db = await getDB(); 19 | return db?.get(STORE_NAME, key); 20 | }, 21 | async set(key: string, value: string) { 22 | const db = await getDB(); 23 | return db?.put(STORE_NAME, value, key); 24 | }, 25 | async clear() { 26 | const db = await getDB(); 27 | return db?.clear(STORE_NAME); 28 | }, 29 | async count() { 30 | const db = await getDB(); 31 | return db?.count(STORE_NAME) ?? 0; 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/services/code-processor/babel.ts: -------------------------------------------------------------------------------- 1 | import * as babel from "@babel/standalone"; 2 | import type { PluginItem } from "@babel/core"; 3 | import { format } from "@/services/code-processor/prettier"; 4 | 5 | export const transform = async (code: string, filename?: string, plugins: PluginItem[] = []) => { 6 | const result = babel.transform(code, { 7 | filename: filename || "main.tsx", 8 | babelrc: false, 9 | plugins: [ 10 | // 11 | ...plugins, 12 | ], 13 | presets: [["typescript", { allExtensions: true, isTSX: true }]], 14 | generatorOpts: { 15 | comments: false, 16 | retainFunctionParens: true, 17 | retainLines: true, 18 | }, 19 | sourceType: "module", 20 | configFile: false, 21 | ast: true, 22 | }); 23 | 24 | if (!result || !result.code) { 25 | throw new Error("Failed to transform code"); 26 | } 27 | 28 | return format(result.code.trim()); 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/layout/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { MoonIcon, SunIcon } from "lucide-react"; 3 | import { useUserStore } from "@/stores/userStore"; 4 | import { Button } from "@/components/ui/button"; 5 | 6 | export function ThemeSwitcher() { 7 | const { theme, setTheme } = useUserStore(); 8 | 9 | useEffect(() => { 10 | const root = window.document.documentElement; 11 | root.classList.remove("light", "dark"); 12 | root.classList.add(theme); 13 | }, [theme]); 14 | 15 | return ( 16 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/common/MonacoTab/MonacoTab.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { MonacoTab } from "./MonacoTab"; 3 | 4 | const meta: Meta = { 5 | title: "Common/MonacoTab", 6 | component: MonacoTab, 7 | }; 8 | export default meta; 9 | 10 | type Story = StoryObj; 11 | 12 | export const Default: Story = { 13 | args: { 14 | tab: { 15 | id: "main.ts", 16 | name: "main.ts", 17 | active: false, 18 | }, 19 | tabs: [ 20 | { id: "main.ts", name: "main.ts", active: false }, 21 | { id: "setup.ts", name: "setup.ts", active: false }, 22 | ], 23 | }, 24 | }; 25 | 26 | export const Active: Story = { 27 | args: { 28 | tab: { 29 | id: "main.ts", 30 | name: "main.ts", 31 | active: true, 32 | }, 33 | tabs: [ 34 | { id: "main.ts", name: "main.ts", active: false }, 35 | { id: "setup.ts", name: "setup.ts", active: false }, 36 | ], 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/stores/userStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist } from "zustand/middleware"; 3 | 4 | const VERTICAL_LAYOUT_MIN_WIDTH = 768; 5 | 6 | export type LayoutMode = "horizontal" | "vertical"; 7 | export type Theme = "dark" | "light"; 8 | 9 | interface UserPreferences { 10 | codeViewLayout: LayoutMode; 11 | theme: Theme; 12 | setCodeViewLayout: (layout: LayoutMode) => void; 13 | setTheme: (theme: Theme) => void; 14 | } 15 | 16 | export const useUserStore = create()( 17 | persist( 18 | (set) => ({ 19 | codeViewLayout: 20 | typeof window === "undefined" ? "vertical" : ( 21 | (window.innerWidth >= VERTICAL_LAYOUT_MIN_WIDTH && "vertical") || "horizontal" 22 | ), 23 | // default to light theme 24 | theme: "light", 25 | setCodeViewLayout: (layout) => set({ codeViewLayout: layout }), 26 | setTheme: (theme) => set({ theme }), 27 | }), 28 | { 29 | name: "user-preferences", 30 | }, 31 | ), 32 | ); 33 | -------------------------------------------------------------------------------- /src/components/common/Logo/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { ZapIcon } from "lucide-react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | interface LogoProps { 5 | size?: "default" | "huge" | "large"; 6 | noIcon?: boolean; 7 | } 8 | 9 | export const Logo = ({ noIcon, size = "default" }: LogoProps) => { 10 | return ( 11 |
12 | {!noIcon && ( 13 | 20 | )} 21 | 28 | Bench 29 | JS 30 | 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/lib/formatters.ts: -------------------------------------------------------------------------------- 1 | export const formatDuration = (ms: number): string => { 2 | if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`; 3 | if (ms >= 1) return `${ms.toFixed(2)}ms`; 4 | if (ms >= 0.001) return `${(ms * 1000).toFixed(2)}µs`; 5 | return `${(ms * 1_000_000).toFixed(2)}ns`; 6 | }; 7 | 8 | export const formatBytes = (bytes: number) => { 9 | if (bytes < 1024) return `${bytes} B`; 10 | const k = 1024; 11 | const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; 12 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 13 | return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`; 14 | }; 15 | 16 | export const formatCount = (num: number): string => { 17 | return new Intl.NumberFormat("en-US").format(Math.floor(num)); 18 | }; 19 | 20 | export const formatCountShort = (value: number): string => { 21 | if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`; 22 | if (value >= 1000) return `${(value / 1000).toFixed(1)}k`; 23 | return value.toString(); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/ExportModal/ExportModal.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { ExportModal } from "./ExportModal"; 3 | 4 | const meta = { 5 | title: "Components/ExportModal", 6 | component: ExportModal, 7 | parameters: { 8 | layout: "centered", 9 | }, 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | const sampleJson = JSON.stringify( 16 | { 17 | results: [ 18 | { 19 | name: "Implementation A", 20 | totalTime: 1234, 21 | opsPerSec: 98_765, 22 | }, 23 | { 24 | name: "Implementation B", 25 | totalTime: 2345, 26 | opsPerSec: 87_654, 27 | }, 28 | ], 29 | }, 30 | null, 31 | 2, 32 | ); 33 | 34 | export const Default: Story = { 35 | args: { 36 | open: true, 37 | value: sampleJson, 38 | onOpenChange: () => {}, 39 | }, 40 | }; 41 | 42 | export const Closed: Story = { 43 | args: { 44 | open: false, 45 | value: sampleJson, 46 | onOpenChange: () => {}, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 3 | import { Check } from "lucide-react"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Checkbox = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 19 | 20 | 21 | 22 | )); 23 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 24 | 25 | export { Checkbox }; 26 | -------------------------------------------------------------------------------- /src/components/playground/code/RunPanel/tabs/ConsoleTab/ConsoleTab.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { ConsoleTab } from "./ConsoleTab"; 3 | 4 | const meta = { 5 | title: "Playground/Code/RunPanel/tabs/ConsoleTab", 6 | component: ConsoleTab, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | type Story = StoryObj; 11 | 12 | const sampleLogs = [ 13 | { 14 | timestamp: Date.now(), 15 | level: "info", 16 | message: "Application started", 17 | count: 1, 18 | }, 19 | { 20 | timestamp: Date.now() + 1000, 21 | level: "warn", 22 | message: "Deprecated feature used", 23 | count: 3, 24 | }, 25 | { 26 | timestamp: Date.now() + 2000, 27 | level: "error", 28 | message: "Failed to fetch data", 29 | count: 1, 30 | }, 31 | { 32 | timestamp: Date.now() + 3000, 33 | level: "debug", 34 | message: "Debug information", 35 | count: 1, 36 | }, 37 | ]; 38 | 39 | export const Default: Story = { 40 | args: { 41 | logs: sampleLogs, 42 | }, 43 | }; 44 | 45 | export const Empty: Story = { 46 | args: { 47 | logs: [], 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/ExportModal/ExportModal.tsx: -------------------------------------------------------------------------------- 1 | import { Monaco } from "@/components/common/Monaco"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; 4 | 5 | interface ExportModalProps { 6 | open: boolean; 7 | onOpenChange: (open: boolean) => void; 8 | value: string; 9 | } 10 | 11 | export function ExportModal({ open, onOpenChange, value }: ExportModalProps) { 12 | return ( 13 | 14 | 15 | 16 | Export Results 17 | 18 |
19 | 28 |
29 | 30 | 31 | 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Switch = React.forwardRef< 6 | React.ElementRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, ...props }, ref) => ( 9 | 17 | 22 | 23 | )); 24 | Switch.displayName = SwitchPrimitives.Root.displayName; 25 | 26 | export { Switch }; 27 | -------------------------------------------------------------------------------- /src/components/common/FileTree/FileTree.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { FileTree } from "./FileTree"; 3 | 4 | const meta: Meta = { 5 | title: "Common/FileTree", 6 | component: FileTree, 7 | }; 8 | export default meta; 9 | 10 | type Story = StoryObj; 11 | 12 | export const Default: Story = { 13 | args: { 14 | item: { 15 | id: "root", 16 | name: "root", 17 | type: "root", 18 | children: [ 19 | { 20 | id: "dist", 21 | name: "dist", 22 | type: "folder", 23 | children: [], 24 | }, 25 | { 26 | id: "node_modules", 27 | name: "node_modules", 28 | type: "folder", 29 | children: [], 30 | count: 42, 31 | }, 32 | { 33 | id: "src", 34 | name: "src", 35 | type: "folder", 36 | children: [ 37 | { 38 | id: "main.ts", 39 | name: "main.ts", 40 | type: "file", 41 | }, 42 | ], 43 | }, 44 | ], 45 | }, 46 | }, 47 | }; 48 | 49 | export const WithActiveFile: Story = { 50 | args: { 51 | ...Default.args, 52 | activeFileId: "main.ts", 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /src/services/code-processor/babel.test.ts: -------------------------------------------------------------------------------- 1 | import { format } from "@/services/code-processor/prettier"; 2 | import { transform } from "./babel"; 3 | 4 | describe("babel", () => { 5 | it("should transform typescript code", async () => { 6 | const code = ` 7 | // comments will be removed 8 | export const run = () => { 9 | return "hello world"; 10 | } 11 | `; 12 | 13 | const result = await transform(code); 14 | expect(result).toBe( 15 | await format(` 16 | export const run = () => { 17 | return "hello world"; 18 | }; 19 | `), 20 | ); 21 | }); 22 | 23 | it("should execute custom plugins", async () => { 24 | const code = ` 25 | type Sample = string; 26 | export const run = () => { 27 | return "hello world"; 28 | } 29 | `; 30 | 31 | const result = await transform(code, "main.ts", [ 32 | { 33 | name: "custom-plugin", 34 | visitor: { 35 | TSTypeAliasDeclaration: { 36 | enter(path) { 37 | path.remove(); 38 | }, 39 | }, 40 | }, 41 | }, 42 | ]); 43 | expect(result).toBe( 44 | await format(` 45 | export const run = () => { 46 | return "hello world"; 47 | }; 48 | `), 49 | ); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const TooltipProvider = TooltipPrimitive.Provider; 6 | const Tooltip = TooltipPrimitive.Root; 7 | const TooltipTrigger = TooltipPrimitive.Trigger; 8 | 9 | const TooltipContent = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, sideOffset = 4, ...props }, ref) => ( 13 | 14 | 23 | 24 | )); 25 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 26 | 27 | export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; 28 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const badgeVariants = cva( 6 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 7 | { 8 | variants: { 9 | variant: { 10 | default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 11 | secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 12 | destructive: 13 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 14 | outline: "text-foreground", 15 | success: 16 | "border-green-600 bg-green-100 text-green-700 hover:bg-green-200 dark:border-green-400 dark:bg-green-800 dark:text-green-300 dark:hover:bg-green-700", 17 | }, 18 | }, 19 | defaultVariants: { 20 | variant: "default", 21 | }, 22 | }, 23 | ); 24 | 25 | export interface BadgeProps 26 | extends React.HTMLAttributes, 27 | VariantProps {} 28 | 29 | function Badge({ className, variant, ...props }: BadgeProps) { 30 | return
; 31 | } 32 | 33 | export { Badge, badgeVariants }; 34 | -------------------------------------------------------------------------------- /src/components/playground/SidebarIcon/SidebarIcon.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | export const SidebarIcon = ({ 5 | icon: Icon, 6 | isActive, 7 | tooltip, 8 | count, 9 | onClick, 10 | }: { 11 | icon: React.ElementType; 12 | isActive: boolean; 13 | tooltip: string; 14 | count?: number; 15 | onClick?: () => void; 16 | }) => { 17 | const [isHovered, setIsHovered] = useState(false); 18 | 19 | return ( 20 |
21 |
setIsHovered(true)} 28 | onMouseLeave={() => setIsHovered(false)} 29 | > 30 | {/* icon */} 31 | 32 | 33 | {/* badge */} 34 | {count && ( 35 | 36 | {count} 37 | 38 | )} 39 |
40 | {isHovered && ( 41 |
42 | {tooltip} 43 |
44 | )} 45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/playground/Sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChartPieIcon, 3 | FolderTreeIcon, 4 | SettingsIcon, 5 | // PackageIcon, 6 | } from "lucide-react"; 7 | import { SidebarIcon } from "@/components/playground/SidebarIcon"; 8 | 9 | export type SidebarTab = "code" | "compare" | "environment" | "settings"; 10 | 11 | export interface SidebarProps { 12 | children?: React.ReactNode; 13 | activeTab: SidebarTab; 14 | onTabChange: (tab: SidebarTab) => void; 15 | } 16 | 17 | export const Sidebar = ({ children, activeTab, onTabChange }: SidebarProps) => { 18 | return ( 19 |
20 |
21 | onTabChange("code")} 26 | /> 27 | onTabChange("compare")} 32 | /> 33 | {/* onTabChange("environment")} */} 38 | {/* /> */} 39 | onTabChange("settings")} 44 | /> 45 |
46 | {children} 47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/common/MonacoTab/TabContextMenu.tsx: -------------------------------------------------------------------------------- 1 | import { MonacoTab } from "@/components/common/MonacoTab"; 2 | import { 3 | ContextMenu, 4 | ContextMenuContent, 5 | ContextMenuItem, 6 | ContextMenuTrigger, 7 | } from "@/components/ui/context-menu"; 8 | 9 | interface TabContextMenuProps { 10 | tab: MonacoTab; 11 | tabs: MonacoTab[]; 12 | onClose: (tab: MonacoTab) => void; 13 | onCloseOthers: (tab: MonacoTab) => void; 14 | onCloseLeft: (tab: MonacoTab) => void; 15 | onCloseRight: (tab: MonacoTab) => void; 16 | children: React.ReactNode; 17 | } 18 | 19 | export const TabContextMenu = ({ 20 | tab, 21 | tabs, 22 | onClose, 23 | onCloseOthers, 24 | onCloseLeft, 25 | onCloseRight, 26 | children, 27 | }: TabContextMenuProps) => { 28 | const tabIndex = tabs.findIndex((t) => t.id === tab.id); 29 | const hasTabsToTheLeft = tabIndex > 0; 30 | const hasTabsToTheRight = tabIndex < tabs.length - 1; 31 | const hasOtherTabs = tabs.length > 1; 32 | 33 | return ( 34 | 35 | {children} 36 | e.stopPropagation()}> 37 | onClose(tab)}>Close 38 | {hasOtherTabs && onCloseOthers(tab)}>Close Others} 39 | {hasTabsToTheLeft && ( 40 | onCloseLeft(tab)}>Close Tabs to the Left 41 | )} 42 | {hasTabsToTheRight && ( 43 | onCloseRight(tab)}>Close Tabs to the Right 44 | )} 45 | 46 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import autoprefixer from "autoprefixer"; 3 | import { copy } from "fs-extra"; 4 | import { existsSync } from "node:fs"; 5 | import { resolve } from "node:path"; 6 | import tailwindcss from "tailwindcss"; 7 | import { defineConfig, PluginOption } from "vite"; 8 | import tsconfigPaths from "vite-tsconfig-paths"; 9 | 10 | const isStorybook = process.argv.some((arg) => arg.includes("storybook")); 11 | 12 | const bundleMonacoEditor = () => { 13 | return { 14 | name: "monaco-plugin", 15 | async buildStart() { 16 | const srcPath = resolve(__dirname, "node_modules/monaco-editor"); 17 | const destPath = resolve(__dirname, "public/monaco-editor"); 18 | if (!existsSync(destPath)) { 19 | await copy(srcPath, destPath, { 20 | dereference: true, 21 | overwrite: true, 22 | }); 23 | console.log("[vite] bundled monaco-editor"); 24 | } 25 | }, 26 | } satisfies PluginOption; 27 | }; 28 | 29 | export default defineConfig({ 30 | css: { 31 | postcss: { 32 | plugins: [tailwindcss, autoprefixer], 33 | }, 34 | }, 35 | worker: { 36 | format: "es", 37 | }, 38 | plugins: [ 39 | // 40 | !isStorybook && reactRouter(), 41 | tsconfigPaths(), 42 | bundleMonacoEditor(), 43 | { 44 | name: "configure-response-headers", 45 | configureServer: (server) => { 46 | server.middlewares.use((_req, _res, next) => { 47 | // res.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); 48 | // res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); 49 | next(); 50 | }); 51 | }, 52 | }, 53 | ], 54 | }); 55 | -------------------------------------------------------------------------------- /src/stores/dependenciesStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { devtools } from "zustand/middleware"; 3 | 4 | export type Dependency = { 5 | name: string; 6 | url: string; 7 | status: "error" | "loading" | "success"; 8 | package?: { 9 | name: string; 10 | version: string; 11 | description: string; 12 | author?: string; 13 | license?: string; 14 | homepage?: string; 15 | }; 16 | error?: string; 17 | }; 18 | 19 | interface DependenciesState { 20 | dependencyMap: Record; 21 | setDependency: (dependency: Dependency) => void; 22 | updateDependency: (dependencyName: string, data: Partial>) => void; 23 | reset: () => void; 24 | } 25 | 26 | export const useDependenciesStore = create()( 27 | devtools( 28 | (set) => ({ 29 | dependencyMap: {}, 30 | setDependency: (dependency) => { 31 | set((state) => ({ 32 | dependencyMap: { 33 | ...state.dependencyMap, 34 | [dependency.name]: dependency, 35 | }, 36 | })); 37 | }, 38 | updateDependency: (dependencyName, data) => { 39 | set((state) => { 40 | const dependency = state.dependencyMap[dependencyName]; 41 | if (!dependency) return state; 42 | return { 43 | dependencyMap: { 44 | ...state.dependencyMap, 45 | [dependencyName]: { 46 | ...dependency, 47 | ...data, 48 | }, 49 | }, 50 | }; 51 | }); 52 | }, 53 | reset: () => { 54 | set({ dependencyMap: {} }); 55 | }, 56 | }), 57 | { name: "dependencies" }, 58 | ), 59 | ); 60 | -------------------------------------------------------------------------------- /src/services/benchmark/types.ts: -------------------------------------------------------------------------------- 1 | import { BenchmarkOptions } from "benchmate"; 2 | 3 | // benchmark types 4 | export type BenchmarkResult = { 5 | name: string; 6 | stats: { 7 | samples: number; 8 | batches: number; 9 | time: { 10 | total: number; 11 | min: number; 12 | max: number; 13 | average: number; 14 | percentile50: number; 15 | percentile90: number; 16 | percentile95: number; 17 | }; 18 | opsPerSecond: { 19 | average: number; 20 | max: number; 21 | min: number; 22 | margin: number; 23 | }; 24 | memory?: number; 25 | }; 26 | }; 27 | 28 | // worker messages 29 | export type MainToWorkerMessage = { 30 | type: "start"; 31 | runs: { 32 | runId: string; 33 | processedCode: string; 34 | }[]; 35 | options?: BenchmarkOptions; 36 | }; 37 | 38 | export type ConsoleLevel = "debug" | "error" | "info" | "log" | "warn"; 39 | 40 | export type WorkerToMainMessage = 41 | | { 42 | type: "consoleBatch"; 43 | runId: string; 44 | logs: { 45 | level: ConsoleLevel; 46 | message: string; 47 | count: number; 48 | }[]; 49 | } 50 | | { type: "error"; runId: string; error: string } 51 | | { 52 | type: "progress"; 53 | runId: string; 54 | elapsedTime: number; 55 | iterationsCompleted: number; 56 | progress: number; 57 | totalIterations: number; 58 | } 59 | | { type: "result"; runId: string; result: BenchmarkResult[] } 60 | | { type: "setup"; runId: string } 61 | | { type: "taskComplete"; runId: string } 62 | | { type: "taskStart"; runId: string } 63 | | { type: "teardown"; runId: string } 64 | | { type: "warmupEnd"; runId: string } 65 | | { type: "warmupStart"; runId: string }; 66 | -------------------------------------------------------------------------------- /src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const ScrollBar = React.forwardRef< 6 | React.ElementRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, orientation = "vertical", ...props }, ref) => ( 9 | 20 | 21 | 22 | )); 23 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 24 | 25 | const ScrollArea = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, children, ...props }, ref) => ( 29 | 30 | 31 | {children} 32 | 33 | 34 | 35 | 36 | )); 37 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 38 | 39 | export { ScrollArea, ScrollBar }; 40 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const README_CONTENT = `# Quick Start Guide 2 | 3 | ## File Structure 4 | - setup.ts: Setup code to prepare data, create helper functions, etc. 5 | - implementations/*.{ts,js}: Implementation files containing benchmarkFn 6 | 7 | ## Writing Benchmarks 8 | 9 | 1. Setup (setup.ts): 10 | 11 | Everything you export will be available as a global for each implementation. 12 | 13 | \`\`\`typescript 14 | // create and export test data 15 | export const data = Array.from({ length: 1000 }, (_, i) => i); 16 | export const sum = (arr: number[]) => arr.reduce((a, b) => a + b, 0); 17 | \`\`\` 18 | 19 | 2. Implementations (example.ts): 20 | 21 | Implementation-specific code goes here, and you need to export a function named 'run'. 22 | 23 | \`\`\`typescript 24 | export const run = () => { 25 | return data.reduce(sum, 0); 26 | } 27 | \`\`\` 28 | `; 29 | 30 | export const DEFAULT_SETUP_CODE = ` 31 | // This is your setup file, use it to prepare data and create shared helpers. 32 | // Everything you export will be available as a global for each implementation. 33 | 34 | // create and export test data 35 | const generateTestData = (size: number) => { 36 | return Array.from({ length: size }, (_, i) => i); 37 | }; 38 | export const data = generateTestData(1000); 39 | 40 | // export a helper function 41 | export const sum = (a: number, b: number) => a + b; 42 | `.trim(); 43 | 44 | export const DEFAULT_SETUP_DTS = ` 45 | declare global { 46 | declare const data: number[]; 47 | declare const sum: (a: number, b: number) => number; 48 | } 49 | export {}; 50 | `.trim(); 51 | 52 | export const DEFAULT_IMPLEMENTATION = ` 53 | // You have access to everything you exported in setup.ts 54 | // All you need to do is export a function named "run" 55 | 56 | export const run = () => { 57 | return data.reduce(sum, 0); 58 | }; 59 | `.trim(); 60 | -------------------------------------------------------------------------------- /src/components/ui/resizable.tsx: -------------------------------------------------------------------------------- 1 | import { GripVertical } from "lucide-react"; 2 | import * as ResizablePrimitive from "react-resizable-panels"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const ResizablePanelGroup = ({ 6 | className, 7 | ...props 8 | }: React.ComponentProps) => ( 9 | 13 | ); 14 | 15 | const ResizablePanel = ResizablePrimitive.Panel; 16 | 17 | const ResizableHandle = ({ 18 | withHandle, 19 | className, 20 | ...props 21 | }: React.ComponentProps & { 22 | withHandle?: boolean; 23 | }) => ( 24 | div]:rotate-90", 27 | className, 28 | )} 29 | {...props} 30 | > 31 | {withHandle && ( 32 |
33 | 34 |
35 | )} 36 |
37 | ); 38 | 39 | export { ResizableHandle, ResizablePanel, ResizablePanelGroup }; 40 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | const Card = React.forwardRef>( 5 | ({ className, ...props }, ref) => ( 6 |
11 | ), 12 | ); 13 | Card.displayName = "Card"; 14 | 15 | const CardHeader = React.forwardRef>( 16 | ({ className, ...props }, ref) => ( 17 |
18 | ), 19 | ); 20 | CardHeader.displayName = "CardHeader"; 21 | 22 | const CardTitle = React.forwardRef>( 23 | ({ className, ...props }, ref) => ( 24 |
25 | ), 26 | ); 27 | CardTitle.displayName = "CardTitle"; 28 | 29 | const CardDescription = React.forwardRef>( 30 | ({ className, ...props }, ref) => ( 31 |
32 | ), 33 | ); 34 | CardDescription.displayName = "CardDescription"; 35 | 36 | const CardContent = React.forwardRef>( 37 | ({ className, ...props }, ref) =>
, 38 | ); 39 | CardContent.displayName = "CardContent"; 40 | 41 | const CardFooter = React.forwardRef>( 42 | ({ className, ...props }, ref) => ( 43 |
44 | ), 45 | ); 46 | CardFooter.displayName = "CardFooter"; 47 | 48 | export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; 49 | -------------------------------------------------------------------------------- /src/components/playground/code/RunPanel/tabs/ConsoleTab/ConsoleTab.tsx: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | import { formatCount } from "@/lib/formatters"; 3 | import { ScrollArea } from "@/components/ui/scroll-area"; 4 | 5 | export interface ConsoleLog { 6 | timestamp: number; 7 | level: string; 8 | message: string; 9 | count: number; 10 | } 11 | 12 | interface ConsoleTabProps { 13 | logs: ConsoleLog[] | null; 14 | } 15 | 16 | const getLogColor = (type: string) => { 17 | switch (type) { 18 | case "error": { 19 | return "text-red-600 dark:text-red-400 font-semibold"; 20 | } 21 | case "warn": { 22 | return "text-yellow-600 dark:text-yellow-400"; 23 | } 24 | case "info": { 25 | return "text-muted-foreground"; 26 | } 27 | case "debug": { 28 | return "text-muted-foreground/70"; 29 | } 30 | default: { 31 | return "text-foreground"; 32 | } 33 | } 34 | }; 35 | 36 | export const ConsoleTab = ({ logs }: ConsoleTabProps) => { 37 | const displayLogs = logs ?? []; 38 | 39 | return ( 40 | 41 |
42 | {displayLogs.length === 0 && ( 43 |
No console output yet.
44 | )} 45 | {displayLogs.map((log, index) => ( 46 | // eslint-disable-next-line react/no-array-index-key 47 |
48 | 49 | {format(log.timestamp, "HH:mm:ss.SSS")} 50 | 51 | {log.message} 52 | {log.count > 1 && ( 53 | 54 | x{formatCount(log.count)} 55 | 56 | )} 57 |
58 | ))} 59 |
60 |
61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BenchJS 2 | 3 | A browser-based JavaScript benchmarking tool: https://benchjs.com 4 | \ 5 | Currently powered by [benchmate](https://github.com/3rd/benchmate). 6 | 7 | ![screenshot](https://root.b-cdn.net/benchjs/Social.png) 8 | 9 | ## Features 10 | 11 | - 🚀 **Zero Setup Required** - Write and run benchmarks directly in your browser 12 | - 📊 **Real-time Metrics** - Watch your benchmarks run with detailed performance statistics 13 | - 🔄 **Easy Comparison** - Compare multiple implementations at once 14 | - 📋 **Modern experience** - TypeScript, Monaco, esbuild, all the goodies 15 | - 📦 **ESM Package Support** - Import packages directly via [esm.sh](https://esm.sh) 16 | - 🔗 **Shareable Results** - Share an URL to your benchmarks or an image of the results 17 | 18 | ## Writing Benchmarks 19 | 20 | ### File Structure 21 | 22 | - `setup.ts`: Setup code and shared utilities 23 | - `implementations/*.ts`: Implementation files containing benchmark code 24 | 25 | ### Setup File 26 | 27 | The setup file (`setup.ts`) is where you prepare data and create shared helpers. Everything you export will be available as a global for each implementation. 28 | 29 | ```typescript 30 | // Generate test data 31 | export const generateData = (size: number) => { 32 | return Array.from({ length: size }, (_, i) => i); 33 | }; 34 | 35 | // Export data and helpers 36 | export const data = generateData(1000); 37 | export const sum = (a: number, b: number) => a + b; 38 | ``` 39 | 40 | ### Implementation Files 41 | 42 | Each implementation file must export a `run` function that contains the code you want to benchmark. 43 | 44 | ```typescript 45 | export const run = () => { 46 | // Your implementation here 47 | return data.reduce(sum, 0); 48 | }; 49 | ``` 50 | 51 | ### Using External Packages 52 | 53 | BenchJS supports importing packages from [esm.sh](https://esm.sh). Configure them on the settings page and then import them: 54 | 55 | ```typescript 56 | import { pick } from "lodash-es"; 57 | 58 | export const run = () => { 59 | return pick(data, ["id", "name"]); 60 | }; 61 | ``` 62 | -------------------------------------------------------------------------------- /src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Tabs = TabsPrimitive.Root; 6 | 7 | const TabsList = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | )); 20 | TabsList.displayName = TabsPrimitive.List.displayName; 21 | 22 | const TabsTrigger = React.forwardRef< 23 | React.ElementRef, 24 | React.ComponentPropsWithoutRef 25 | >(({ className, ...props }, ref) => ( 26 | 34 | )); 35 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; 36 | 37 | const TabsContent = React.forwardRef< 38 | React.ElementRef, 39 | React.ComponentPropsWithoutRef 40 | >(({ className, ...props }, ref) => ( 41 | 49 | )); 50 | TabsContent.displayName = TabsPrimitive.Content.displayName; 51 | 52 | export { Tabs, TabsContent, TabsList, TabsTrigger }; 53 | -------------------------------------------------------------------------------- /src/components/layout/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Link } from "react-router"; 3 | import { cn } from "@/lib/utils"; 4 | import { Logo } from "@/components/common/Logo"; 5 | import { ThemeSwitcher } from "@/components/layout/ThemeSwitcher"; 6 | import { Button } from "@/components/ui/button"; 7 | 8 | export interface HeaderProps { 9 | className?: string; 10 | customNav?: React.ReactNode; 11 | postLogoElement?: React.ReactNode; 12 | } 13 | 14 | export const Header = ({ postLogoElement, customNav, className }: HeaderProps) => { 15 | const [isScrolled, setIsScrolled] = useState(false); 16 | 17 | useEffect(() => { 18 | const handleScroll = () => { 19 | setIsScrolled(window.scrollY > 0); 20 | }; 21 | 22 | window.addEventListener("scroll", handleScroll); 23 | return () => window.removeEventListener("scroll", handleScroll); 24 | }, []); 25 | 26 | return ( 27 |
34 |
35 | {/* logo */} 36 |
37 | 38 | 39 | 40 | {postLogoElement} 41 |
42 | 43 | {/* navigation */} 44 | 62 |
63 |
64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | darkMode: ["class"], 5 | content: ["./src/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"], 6 | theme: { 7 | extend: { 8 | fontFamily: { 9 | sans: [ 10 | "Inter", 11 | "ui-sans-serif", 12 | "system-ui", 13 | "sans-serif", 14 | '"Apple Color Emoji"', 15 | '"Segoe UI Emoji"', 16 | '"Segoe UI Symbol"', 17 | '"Noto Color Emoji"', 18 | ], 19 | mono: ["IBM Plex Mono", "ui-monospace", "monospace"], 20 | }, 21 | borderRadius: { 22 | lg: "var(--radius)", 23 | md: "calc(var(--radius) - 2px)", 24 | sm: "calc(var(--radius) - 4px)", 25 | xl: "calc(var(--radius) + 0.25rem)", 26 | }, 27 | colors: { 28 | background: "hsl(var(--background))", 29 | foreground: "hsl(var(--foreground))", 30 | card: { 31 | DEFAULT: "hsl(var(--card))", 32 | foreground: "hsl(var(--card-foreground))", 33 | }, 34 | popover: { 35 | DEFAULT: "hsl(var(--popover))", 36 | foreground: "hsl(var(--popover-foreground))", 37 | }, 38 | primary: { 39 | DEFAULT: "hsl(var(--primary))", 40 | foreground: "hsl(var(--primary-foreground))", 41 | }, 42 | secondary: { 43 | DEFAULT: "hsl(var(--secondary))", 44 | foreground: "hsl(var(--secondary-foreground))", 45 | }, 46 | muted: { 47 | DEFAULT: "hsl(var(--muted))", 48 | foreground: "hsl(var(--muted-foreground))", 49 | }, 50 | accent: { 51 | DEFAULT: "hsl(var(--accent))", 52 | foreground: "hsl(var(--accent-foreground))", 53 | }, 54 | destructive: { 55 | DEFAULT: "hsl(var(--destructive))", 56 | foreground: "hsl(var(--destructive-foreground))", 57 | }, 58 | border: "hsl(var(--border))", 59 | input: "hsl(var(--input))", 60 | ring: "hsl(var(--ring))", 61 | chart: { 62 | "1": "hsl(var(--chart-1))", 63 | "2": "hsl(var(--chart-2))", 64 | "3": "hsl(var(--chart-3))", 65 | "4": "hsl(var(--chart-4))", 66 | "5": "hsl(var(--chart-5))", 67 | }, 68 | }, 69 | }, 70 | }, 71 | plugins: [require("tailwindcss-animate")], 72 | } satisfies Config; 73 | -------------------------------------------------------------------------------- /src/components/playground/compare/ComparisonChart/ComparisonChart.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { 3 | Bar, 4 | CartesianGrid, 5 | Legend, 6 | BarChart as RechartsBarChart, 7 | ResponsiveContainer, 8 | Tooltip, 9 | XAxis, 10 | YAxis, 11 | } from "recharts"; 12 | import { BenchmarkRun } from "@/stores/benchmarkStore"; 13 | import { Implementation } from "@/stores/persistentStore"; 14 | import { formatCountShort } from "@/lib/formatters"; 15 | 16 | interface ComparisonChartProps { 17 | implementations: Implementation[]; 18 | runs: Record; 19 | } 20 | 21 | export const ComparisonChart = ({ implementations, runs }: ComparisonChartProps) => { 22 | const barData = useMemo(() => { 23 | return implementations.map((item) => { 24 | const run = runs[item.id]?.at(-1); 25 | const isRunning = run?.status === "running" || run?.status === "warmup"; 26 | const opsPerSec = 27 | isRunning && run.completedIterations ? 28 | run.completedIterations / (run.elapsedTime / 1000) 29 | : run?.result?.stats.opsPerSecond.average || 0; 30 | 31 | return { 32 | name: item.filename, 33 | "Operations/sec": opsPerSec, 34 | }; 35 | }); 36 | }, [implementations, runs]); 37 | 38 | return ( 39 |
40 |
41 | 42 | 43 | 44 | 45 | 57 | `${Math.round(value).toLocaleString()} ops/sec`} 65 | /> 66 | 67 | 68 | 69 | 70 |
71 |
72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | import { cn } from "@/lib/utils"; 5 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-40 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90 active:bg-destructive/80", 15 | outline: "border-2 border-border bg-transparent text-foreground hover:bg-accent active:bg-muted", 16 | secondary: "bg-secondary text-secondary-foreground hover:bg-muted active:bg-muted/80", 17 | ghost: "hover:bg-accent hover:text-accent-foreground active:bg-muted", 18 | link: "text-foreground underline-offset-4 hover:underline", 19 | }, 20 | size: { 21 | default: "h-8 px-3 py-1.5", 22 | sm: "h-7 rounded-md px-2.5 text-xs", 23 | lg: "h-10 rounded-lg px-6", 24 | icon: "h-8 w-8", 25 | }, 26 | }, 27 | defaultVariants: { 28 | variant: "default", 29 | size: "default", 30 | }, 31 | }, 32 | ); 33 | 34 | export interface ButtonProps 35 | extends React.ButtonHTMLAttributes, 36 | VariantProps { 37 | asChild?: boolean; 38 | tooltip?: string; 39 | } 40 | 41 | const Button = React.forwardRef( 42 | ({ className, variant, size, asChild = false, tooltip, ...props }, ref) => { 43 | const Component = asChild ? Slot : "button"; 44 | const button = ( 45 | 46 | ); 47 | 48 | if (tooltip) { 49 | return ( 50 | 51 | 52 | {button} 53 | {tooltip} 54 | 55 | 56 | ); 57 | } 58 | 59 | return button; 60 | }, 61 | ); 62 | Button.displayName = "Button"; 63 | 64 | export { Button, buttonVariants }; 65 | -------------------------------------------------------------------------------- /src/components/playground/ShareDialog/ShareDialog.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import type { BenchmarkStatus } from "@/stores/benchmarkStore"; 3 | import { ShareDialog } from "./ShareDialog"; 4 | 5 | const mockImplementations = [ 6 | { id: "1", filename: "quicksort.ts", content: "// implementation" }, 7 | { id: "2", filename: "mergesort.ts", content: "// implementation" }, 8 | { id: "3", filename: "bubblesort.ts", content: "// implementation" }, 9 | ]; 10 | 11 | const createBenchmarkRun = (id: string, implId: string, filename: string, ops: number) => ({ 12 | id, 13 | implementationId: implId, 14 | filename, 15 | originalCode: "// implementation", 16 | processedCode: "// processed implementation", 17 | status: "completed" as BenchmarkStatus, 18 | progress: 100, 19 | createdAt: Date.now(), 20 | warmupStartedAt: Date.now(), 21 | warmupEndedAt: Date.now(), 22 | runStartedAt: Date.now(), 23 | runEndedAt: Date.now(), 24 | elapsedTime: 1000, 25 | error: null, 26 | completedIterations: 1000, 27 | totalIterations: 1000, 28 | result: { 29 | name: filename, 30 | stats: { 31 | samples: 100, 32 | batches: 10, 33 | time: { 34 | total: 1000, 35 | min: 900, 36 | max: 1100, 37 | average: 1000, 38 | percentile50: 1000, 39 | percentile90: 1050, 40 | percentile95: 1075, 41 | }, 42 | opsPerSecond: { 43 | average: ops, 44 | max: ops * 1.1, 45 | min: ops * 0.9, 46 | margin: 0.01, 47 | }, 48 | }, 49 | }, 50 | }); 51 | 52 | const mockRuns = { 53 | "1": [createBenchmarkRun("run1", "1", "quicksort.ts", 15_000)], 54 | "2": [createBenchmarkRun("run2", "2", "mergesort.ts", 12_000)], 55 | }; 56 | 57 | const meta = { 58 | title: "Playground/ShareDialog", 59 | component: ShareDialog, 60 | parameters: { 61 | layout: "centered", 62 | }, 63 | args: { 64 | open: true, 65 | onOpenChange: () => {}, 66 | shareUrl: "http://localhost:3000/#code=example", 67 | }, 68 | } satisfies Meta; 69 | 70 | export default meta; 71 | type Story = StoryObj; 72 | 73 | export const Default: Story = { 74 | args: { 75 | implementations: mockImplementations, 76 | runs: mockRuns, 77 | }, 78 | }; 79 | 80 | export const NoRuns: Story = { 81 | args: { 82 | implementations: mockImplementations, 83 | runs: {}, 84 | }, 85 | }; 86 | 87 | export const SingleRun: Story = { 88 | args: { 89 | implementations: mockImplementations, 90 | runs: { 91 | "1": [createBenchmarkRun("run1", "1", "quicksort.ts", 15_000)], 92 | }, 93 | }, 94 | }; 95 | -------------------------------------------------------------------------------- /src/components/common/MonacoTab/MonacoTab.tsx: -------------------------------------------------------------------------------- 1 | import { useSortable } from "@dnd-kit/sortable"; 2 | import { CSS } from "@dnd-kit/utilities"; 3 | import { XIcon } from "lucide-react"; 4 | import { cn } from "@/lib/utils"; 5 | import { Button } from "@/components/ui/button"; 6 | import { TabContextMenu } from "./TabContextMenu"; 7 | 8 | export type MonacoTab = { 9 | id: string; 10 | name: string; 11 | active: boolean; 12 | }; 13 | 14 | interface TabProps { 15 | tab: MonacoTab; 16 | tabs: MonacoTab[]; 17 | onClose?: (file: MonacoTab) => void; 18 | onCloseOthers?: (file: MonacoTab) => void; 19 | onCloseLeft?: (file: MonacoTab) => void; 20 | onCloseRight?: (file: MonacoTab) => void; 21 | onClick?: (file: MonacoTab) => void; 22 | } 23 | 24 | export const MonacoTab = ({ 25 | tab, 26 | tabs, 27 | onClose, 28 | onCloseOthers, 29 | onCloseLeft, 30 | onCloseRight, 31 | onClick, 32 | }: TabProps) => { 33 | const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ 34 | id: tab.name, 35 | transition: { 36 | duration: 150, 37 | easing: "cubic-bezier(0.25, 1, 0.5, 1)", 38 | }, 39 | }); 40 | 41 | const style = { 42 | transform: CSS.Translate.toString(transform), 43 | transition, 44 | opacity: isDragging ? 0.5 : undefined, 45 | zIndex: isDragging ? 1 : undefined, 46 | position: isDragging ? ("relative" as const) : undefined, 47 | }; 48 | 49 | const handleClick = () => { 50 | onClick?.(tab); 51 | }; 52 | 53 | const tabContent = ( 54 |
55 | {tab.name} 56 | 67 |
68 | ); 69 | 70 | return ( 71 | {})} 75 | onCloseLeft={onCloseLeft ?? (() => {})} 76 | onCloseOthers={onCloseOthers ?? (() => {})} 77 | onCloseRight={onCloseRight ?? (() => {})} 78 | > 79 |
92 | {tabContent} 93 |
94 |
95 | ); 96 | }; 97 | -------------------------------------------------------------------------------- /src/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | @apply bg-background text-foreground; 8 | 9 | @media (prefers-color-scheme: dark) { 10 | color-scheme: dark; 11 | } 12 | } 13 | 14 | @layer base { 15 | :root { 16 | --background: 0 0% 100%; 17 | --foreground: 0 0% 9%; 18 | --card: 0 0% 100%; 19 | --card-foreground: 0 0% 9%; 20 | --popover: 0 0% 100%; 21 | --popover-foreground: 0 0% 9%; 22 | --primary: 0 0% 18%; 23 | --primary-foreground: 0 0% 100%; 24 | --secondary: 0 0% 96%; 25 | --secondary-foreground: 0 0% 9%; 26 | --muted: 0 0% 96%; 27 | --muted-foreground: 0 0% 45%; 28 | --accent: 0 0% 93%; 29 | --accent-foreground: 0 0% 9%; 30 | --destructive: 0 84% 60%; 31 | --destructive-foreground: 0 0% 100%; 32 | --border: 0 0% 90%; 33 | --input: 0 0% 90%; 34 | --ring: 0 0% 18%; 35 | --chart-1: 0 0% 20%; 36 | --chart-2: 0 0% 40%; 37 | --chart-3: 0 0% 50%; 38 | --chart-4: 0 0% 70%; 39 | --chart-5: 0 0% 85%; 40 | --radius: 0.75rem; 41 | } 42 | .dark { 43 | --background: 0 0% 9%; 44 | --foreground: 0 0% 98%; 45 | --card: 0 0% 12%; 46 | --card-foreground: 0 0% 98%; 47 | --popover: 0 0% 12%; 48 | --popover-foreground: 0 0% 98%; 49 | --primary: 0 0% 90%; 50 | --primary-foreground: 0 0% 9%; 51 | --secondary: 0 0% 14%; 52 | --secondary-foreground: 0 0% 98%; 53 | --muted: 0 0% 14%; 54 | --muted-foreground: 0 0% 60%; 55 | --accent: 0 0% 18%; 56 | --accent-foreground: 0 0% 98%; 57 | --destructive: 0 63% 51%; 58 | --destructive-foreground: 0 0% 98%; 59 | --border: 0 0% 18%; 60 | --input: 0 0% 18%; 61 | --ring: 0 0% 90%; 62 | --chart-1: 0 0% 90%; 63 | --chart-2: 0 0% 70%; 64 | --chart-3: 0 0% 50%; 65 | --chart-4: 0 0% 30%; 66 | --chart-5: 0 0% 15%; 67 | } 68 | } 69 | 70 | @layer base { 71 | * { 72 | @apply border-border; 73 | } 74 | body { 75 | @apply bg-background text-foreground; 76 | } 77 | } 78 | 79 | .custom-scrollbar { 80 | scrollbar-gutter: stable; 81 | overflow: auto; 82 | } 83 | 84 | .custom-scrollbar::-webkit-scrollbar { 85 | width: 4px; 86 | height: 4px; 87 | } 88 | 89 | .custom-scrollbar::-webkit-scrollbar-track { 90 | background: transparent; 91 | } 92 | 93 | .custom-scrollbar::-webkit-scrollbar-thumb { 94 | background-color: rgb(203 213 225 / 0.5); 95 | border-radius: 8px; 96 | transition: background-color 200ms; 97 | } 98 | 99 | .custom-scrollbar:hover::-webkit-scrollbar-thumb { 100 | background-color: rgb(148 163 184 / 0.9); 101 | } 102 | 103 | .dark .custom-scrollbar::-webkit-scrollbar-thumb { 104 | background-color: rgb(71 85 105 / 0.5); 105 | } 106 | 107 | .dark .custom-scrollbar:hover::-webkit-scrollbar-thumb { 108 | background-color: rgb(100 116 139 / 0.9); 109 | } 110 | 111 | .deep-overflow-visible * { 112 | overflow: visible; 113 | } 114 | -------------------------------------------------------------------------------- /src/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | const Table = React.forwardRef>( 5 | ({ className, ...props }, ref) => ( 6 |
7 | 8 | 9 | ), 10 | ); 11 | Table.displayName = "Table"; 12 | 13 | const TableHeader = React.forwardRef>( 14 | ({ className, ...props }, ref) => ( 15 | 16 | ), 17 | ); 18 | TableHeader.displayName = "TableHeader"; 19 | 20 | const TableBody = React.forwardRef>( 21 | ({ className, ...props }, ref) => ( 22 | 23 | ), 24 | ); 25 | TableBody.displayName = "TableBody"; 26 | 27 | const TableFooter = React.forwardRef>( 28 | ({ className, ...props }, ref) => ( 29 | tr]:last:border-b-0", className)} 32 | {...props} 33 | /> 34 | ), 35 | ); 36 | TableFooter.displayName = "TableFooter"; 37 | 38 | const TableRow = React.forwardRef>( 39 | ({ className, ...props }, ref) => ( 40 | 45 | ), 46 | ); 47 | TableRow.displayName = "TableRow"; 48 | 49 | const TableHead = React.forwardRef>( 50 | ({ className, ...props }, ref) => ( 51 |
[role=checkbox]]:translate-y-[2px]", 55 | className, 56 | )} 57 | {...props} 58 | /> 59 | ), 60 | ); 61 | TableHead.displayName = "TableHead"; 62 | 63 | const TableCell = React.forwardRef>( 64 | ({ className, ...props }, ref) => ( 65 | [role=checkbox]]:translate-y-[2px]", 69 | className, 70 | )} 71 | {...props} 72 | /> 73 | ), 74 | ); 75 | TableCell.displayName = "TableCell"; 76 | 77 | const TableCaption = React.forwardRef>( 78 | ({ className, ...props }, ref) => ( 79 |
80 | ), 81 | ); 82 | TableCaption.displayName = "TableCaption"; 83 | 84 | export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow }; 85 | -------------------------------------------------------------------------------- /src/components/playground/compare/ComparisonChart/ComparisonChart.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { BenchmarkStatus } from "@/stores/benchmarkStore"; 3 | import { ComparisonChart } from "./ComparisonChart"; 4 | 5 | const meta = { 6 | title: "Playground/Compare/ComparisonChart", 7 | component: ComparisonChart, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | const mockImplementations = [ 14 | { 15 | id: "1", 16 | filename: "implementation1.ts", 17 | content: 'console.log("test")', 18 | }, 19 | { 20 | id: "2", 21 | filename: "implementation2.ts", 22 | content: 'console.log("test2")', 23 | }, 24 | ]; 25 | 26 | const now = Date.now(); 27 | 28 | const mockRuns = { 29 | "1": [ 30 | { 31 | id: "run1", 32 | implementationId: "1", 33 | status: "completed" as BenchmarkStatus, 34 | filename: "implementation1.ts", 35 | originalCode: 'console.log("test")', 36 | processedCode: 'console.log("test")', 37 | elapsedTime: 1000, 38 | completedIterations: 100_000, 39 | totalIterations: 1000, 40 | progress: 100, 41 | createdAt: now, 42 | warmupStartedAt: now + 100, 43 | warmupEndedAt: now + 200, 44 | error: null, 45 | result: { 46 | name: "implementation1.ts", 47 | stats: { 48 | samples: 100, 49 | batches: 10, 50 | time: { 51 | total: 1000, 52 | min: 900, 53 | max: 1100, 54 | average: 1000, 55 | percentile50: 1000, 56 | percentile90: 1050, 57 | percentile95: 1075, 58 | }, 59 | opsPerSecond: { 60 | average: 200, 61 | max: 1100, 62 | min: 900, 63 | margin: 50, 64 | }, 65 | memory: 1024, 66 | }, 67 | }, 68 | }, 69 | ], 70 | "2": [ 71 | { 72 | id: "run2", 73 | implementationId: "2", 74 | status: "running" as BenchmarkStatus, 75 | filename: "implementation2.ts", 76 | originalCode: 'console.log("test2")', 77 | processedCode: 'console.log("test2")', 78 | elapsedTime: 500, 79 | completedIterations: 500, 80 | totalIterations: 1000, 81 | progress: 50, 82 | createdAt: now, 83 | warmupStartedAt: now + 100, 84 | warmupEndedAt: null, 85 | error: null, 86 | result: { 87 | name: "implementation2.ts", 88 | stats: { 89 | samples: 50, 90 | batches: 5, 91 | time: { 92 | total: 500, 93 | min: 450, 94 | max: 550, 95 | average: 500, 96 | percentile50: 500, 97 | percentile90: 525, 98 | percentile95: 537, 99 | }, 100 | opsPerSecond: { 101 | average: 1500, 102 | max: 550, 103 | min: 450, 104 | margin: 25, 105 | }, 106 | memory: 512, 107 | }, 108 | }, 109 | }, 110 | ], 111 | }; 112 | 113 | export const Default: Story = { 114 | args: { 115 | implementations: mockImplementations, 116 | runs: mockRuns, 117 | }, 118 | }; 119 | -------------------------------------------------------------------------------- /src/components/playground/code/RunPanel/tabs/RunTab/RunTab.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { RunTab } from "./RunTab"; 3 | 4 | const meta = { 5 | title: "Playground/Code/RunPanel/tabs/RunTab", 6 | component: RunTab, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | type Story = StoryObj; 11 | 12 | const mockChartData = [ 13 | { time: 0, timePerOp: 12, iterations: 0 }, 14 | { time: 100, timePerOp: 11, iterations: 100 }, 15 | { time: 200, timePerOp: 10.5, iterations: 200 }, 16 | { time: 300, timePerOp: 10.2, iterations: 300 }, 17 | { time: 400, timePerOp: 10.1, iterations: 400 }, 18 | ]; 19 | 20 | const now = Date.now(); 21 | 22 | export const Running: Story = { 23 | args: { 24 | isRunning: true, 25 | latestRun: { 26 | id: "1", 27 | implementationId: "impl-1", 28 | createdAt: now, 29 | warmupStartedAt: now + 100, 30 | warmupEndedAt: now + 200, 31 | status: "running", 32 | progress: 45.5, 33 | completedIterations: 455, 34 | totalIterations: 1000, 35 | elapsedTime: 4550, 36 | error: null, 37 | result: null, 38 | filename: "test.js", 39 | originalCode: "function test() {}", 40 | processedCode: "function test() {}", 41 | }, 42 | chartData: mockChartData, 43 | clearChartData: () => {}, 44 | }, 45 | }; 46 | 47 | export const Completed: Story = { 48 | args: { 49 | isRunning: false, 50 | latestRun: { 51 | id: "1", 52 | implementationId: "impl-1", 53 | createdAt: now, 54 | warmupStartedAt: now + 100, 55 | warmupEndedAt: now + 200, 56 | status: "completed", 57 | progress: 100, 58 | completedIterations: 1000, 59 | totalIterations: 1000, 60 | elapsedTime: 10_000, 61 | error: null, 62 | filename: "test.js", 63 | originalCode: "function test() {}", 64 | processedCode: "function test() {}", 65 | result: { 66 | name: "test", 67 | stats: { 68 | samples: 1000, 69 | batches: 10, 70 | time: { 71 | total: 10_000, 72 | average: 10, 73 | min: 8, 74 | max: 15, 75 | percentile50: 10, 76 | percentile90: 13, 77 | percentile95: 14, 78 | }, 79 | opsPerSecond: { 80 | average: 100_000, 81 | min: 66_666, 82 | max: 125_000, 83 | margin: 0.5, 84 | }, 85 | }, 86 | }, 87 | }, 88 | chartData: mockChartData, 89 | clearChartData: () => {}, 90 | }, 91 | }; 92 | 93 | export const Error: Story = { 94 | args: { 95 | isRunning: false, 96 | latestRun: { 97 | id: "1", 98 | implementationId: "impl-1", 99 | createdAt: now, 100 | warmupStartedAt: now + 100, 101 | warmupEndedAt: now + 200, 102 | status: "failed", 103 | progress: 45.5, 104 | completedIterations: 455, 105 | totalIterations: 1000, 106 | elapsedTime: 4550, 107 | error: "Failed to execute benchmark: Stack overflow", 108 | filename: "test.js", 109 | originalCode: "function test() {}", 110 | processedCode: "function test() {}", 111 | result: null, 112 | }, 113 | chartData: mockChartData, 114 | clearChartData: () => {}, 115 | }, 116 | }; 117 | -------------------------------------------------------------------------------- /src/components/playground/compare/ComparisonTable/ComparisonTable.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { BenchmarkStatus } from "@/stores/benchmarkStore"; 3 | import { ComparisonTable } from "./ComparisonTable"; 4 | 5 | const meta = { 6 | title: "Playground/Compare/ComparisonTable", 7 | component: ComparisonTable, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | const mockImplementations = [ 14 | { 15 | id: "1", 16 | filename: "implementation1.ts", 17 | content: 'console.log("test")', 18 | selected: false, 19 | }, 20 | { 21 | id: "2", 22 | filename: "implementation2.ts", 23 | content: 'console.log("test2")', 24 | selected: true, 25 | }, 26 | ]; 27 | 28 | const now = Date.now(); 29 | 30 | const mockRuns = { 31 | "1": [ 32 | { 33 | id: "run1", 34 | implementationId: "1", 35 | status: "completed" as BenchmarkStatus, 36 | filename: "implementation1.ts", 37 | originalCode: 'console.log("test")', 38 | processedCode: 'console.log("test")', 39 | elapsedTime: 1000, 40 | completedIterations: 1000, 41 | totalIterations: 1000, 42 | progress: 100, 43 | createdAt: now, 44 | warmupStartedAt: now + 100, 45 | warmupEndedAt: now + 200, 46 | error: null, 47 | result: { 48 | name: "implementation1.ts", 49 | stats: { 50 | samples: 100, 51 | batches: 10, 52 | time: { 53 | total: 1000, 54 | min: 900, 55 | max: 1100, 56 | average: 1000, 57 | percentile50: 1000, 58 | percentile90: 1050, 59 | percentile95: 1075, 60 | }, 61 | opsPerSecond: { 62 | average: 1000, 63 | max: 1100, 64 | min: 900, 65 | margin: 50, 66 | }, 67 | memory: 1024, 68 | }, 69 | }, 70 | }, 71 | ], 72 | "2": [ 73 | { 74 | id: "run2", 75 | implementationId: "2", 76 | status: "running" as BenchmarkStatus, 77 | filename: "implementation2.ts", 78 | originalCode: 'console.log("test2")', 79 | processedCode: 'console.log("test2")', 80 | elapsedTime: 500, 81 | completedIterations: 500, 82 | totalIterations: 1000, 83 | progress: 50, 84 | createdAt: now, 85 | warmupStartedAt: now + 100, 86 | warmupEndedAt: null, 87 | error: null, 88 | result: { 89 | name: "implementation2.ts", 90 | stats: { 91 | samples: 50, 92 | batches: 5, 93 | time: { 94 | total: 500, 95 | min: 450, 96 | max: 550, 97 | average: 500, 98 | percentile50: 500, 99 | percentile90: 525, 100 | percentile95: 537, 101 | }, 102 | opsPerSecond: { 103 | average: 500, 104 | max: 550, 105 | min: 450, 106 | margin: 25, 107 | }, 108 | memory: 512, 109 | }, 110 | }, 111 | }, 112 | ], 113 | }; 114 | 115 | export const Default: Story = { 116 | args: { 117 | implementations: mockImplementations, 118 | runs: mockRuns, 119 | isRunning: true, 120 | onSelectAll: () => {}, 121 | onToggleSelect: () => {}, 122 | onRunSingle: () => {}, 123 | onStop: () => {}, 124 | }, 125 | }; 126 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "benchjs", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "cross-env NODE_ENV=production react-router build", 7 | "dev": "react-router dev", 8 | "start": "cross-env NODE_ENV=production react-router-serve ./build/server/index.js", 9 | "typecheck": "react-router typegen && tsc", 10 | "storybook": "storybook dev -p 6006", 11 | "build-storybook": "storybook build", 12 | "test": "vitest run", 13 | "test:watch": "vitest watch", 14 | "test:ui": "vitest --ui", 15 | "test:coverage": "vitest run --coverage" 16 | }, 17 | "dependencies": { 18 | "@babel/core": "7.26.0", 19 | "@babel/standalone": "^7.26.4", 20 | "@dnd-kit/core": "^6.3.1", 21 | "@dnd-kit/modifiers": "^9.0.0", 22 | "@dnd-kit/sortable": "^10.0.0", 23 | "@dnd-kit/utilities": "^3.2.2", 24 | "@hookform/resolvers": "^3.9.1", 25 | "@icons-pack/react-simple-icons": "^10.2.0", 26 | "@monaco-editor/react": "^4.6.0", 27 | "@radix-ui/react-checkbox": "^1.1.3", 28 | "@radix-ui/react-context-menu": "^2.2.4", 29 | "@radix-ui/react-dialog": "^1.1.4", 30 | "@radix-ui/react-dropdown-menu": "^2.1.4", 31 | "@radix-ui/react-label": "^2.1.1", 32 | "@radix-ui/react-progress": "^1.1.1", 33 | "@radix-ui/react-scroll-area": "^1.2.2", 34 | "@radix-ui/react-select": "^2.1.4", 35 | "@radix-ui/react-slot": "^1.1.1", 36 | "@radix-ui/react-switch": "^1.1.2", 37 | "@radix-ui/react-tabs": "^1.1.2", 38 | "@radix-ui/react-tooltip": "^1.1.6", 39 | "@react-router/node": "^7.1.0", 40 | "@react-router/serve": "^7.1.0", 41 | "@types/semver": "^7.7.1", 42 | "@typescript/ata": "^0.9.7", 43 | "benchmate": "^1.12.0", 44 | "class-variance-authority": "^0.7.1", 45 | "clsx": "^2.1.1", 46 | "date-fns": "^4.1.0", 47 | "es-toolkit": "^1.30.1", 48 | "esbuild-wasm": "^0.24.2", 49 | "framer-motion": "^11.15.0", 50 | "html-to-image": "^1.11.11", 51 | "idb": "^8.0.1", 52 | "isbot": "^5.1.18", 53 | "lucide-react": "^0.469.0", 54 | "lz-string": "^1.5.0", 55 | "monaco-editor": "^0.52.2", 56 | "nanoid": "^5.0.9", 57 | "prettier": "^3.4.2", 58 | "react": "^19.0.0", 59 | "react-dom": "^19.0.0", 60 | "react-hook-form": "^7.54.2", 61 | "react-intersection-observer": "^9.14.0", 62 | "react-resizable-panels": "^2.1.7", 63 | "react-router": "^7.1.0", 64 | "recharts": "^2.15.0", 65 | "semver": "^7.7.3", 66 | "serialize-error": "^11.0.3", 67 | "tailwind-merge": "^2.5.5", 68 | "tailwindcss-animate": "^1.0.7", 69 | "typescript": "^5.7.2", 70 | "zod": "^3.24.1", 71 | "zustand": "^5.0.2" 72 | }, 73 | "devDependencies": { 74 | "@react-router/dev": "^7.1.0", 75 | "@storybook/addon-essentials": "^8.4.7", 76 | "@storybook/addon-interactions": "^8.4.7", 77 | "@storybook/blocks": "^8.4.7", 78 | "@storybook/react": "^8.4.7", 79 | "@storybook/react-vite": "^8.4.7", 80 | "@storybook/test": "^8.4.7", 81 | "@types/babel__core": "^7.20.5", 82 | "@types/babel__standalone": "^7.1.9", 83 | "@types/babel__traverse": "^7.20.6", 84 | "@types/fs-extra": "^11.0.4", 85 | "@types/node": "^22.10.2", 86 | "@types/react": "^19.0.2", 87 | "@types/react-dom": "^19.0.2", 88 | "@vitest/coverage-v8": "^2.1.8", 89 | "@vitest/ui": "^2.1.8", 90 | "autoprefixer": "^10.4.20", 91 | "cross-env": "^7.0.3", 92 | "fs-extra": "^11.2.0", 93 | "postcss": "^8.4.49", 94 | "storybook": "^8.4.7", 95 | "storybook-addon-remix-react-router": "3.0.3--canary.85.7d8672d.0", 96 | "tailwindcss": "^3.4.17", 97 | "vite": "^6.0.5", 98 | "vite-tsconfig-paths": "^5.1.4", 99 | "vitest": "^2.1.8" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/root.tsx: -------------------------------------------------------------------------------- 1 | import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; 2 | import type { Route } from "../src/+types/root"; 3 | import stylesheet from "./global.css?url"; 4 | 5 | export const links: Route.LinksFunction = () => [ 6 | { rel: "preconnect", href: "https://fonts.googleapis.com" }, 7 | { 8 | rel: "preconnect", 9 | href: "https://fonts.gstatic.com", 10 | crossOrigin: "anonymous", 11 | }, 12 | { 13 | rel: "stylesheet", 14 | href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", 15 | }, 16 | { 17 | rel: "stylesheet", 18 | href: "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap", 19 | }, 20 | { rel: "stylesheet", href: stylesheet }, 21 | ]; 22 | 23 | export function Layout({ children }: { children: React.ReactNode }) { 24 | return ( 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |