Repository: solidjs/solid-playground Branch: main Commit: 4d6a22c5a338 Files: 63 Total size: 142.8 KB Directory structure: gitextract_mlfkxwgk/ ├── .gitignore ├── .oxfmtrc.json ├── .oxlintrc.json ├── LICENSE ├── README.md ├── package.json ├── packages/ │ ├── playground/ │ │ ├── index.html │ │ ├── package.json │ │ ├── public/ │ │ │ ├── _redirects │ │ │ ├── manifest.webmanifest │ │ │ ├── robots.txt │ │ │ └── sw.js │ │ ├── src/ │ │ │ ├── app.tsx │ │ │ ├── components/ │ │ │ │ ├── header.tsx │ │ │ │ ├── setupSolid.ts │ │ │ │ ├── update.tsx │ │ │ │ └── zoomDropdown.tsx │ │ │ ├── context.tsx │ │ │ ├── index.tsx │ │ │ ├── pages/ │ │ │ │ ├── edit.tsx │ │ │ │ ├── home.tsx │ │ │ │ └── login.tsx │ │ │ └── utils/ │ │ │ ├── date.ts │ │ │ ├── exportFiles.tsx │ │ │ ├── isDarkTheme.ts │ │ │ └── serviceWorker.ts │ │ ├── tsconfig.json │ │ ├── unocss.config.ts │ │ └── vite.config.ts │ └── solid-repl/ │ ├── build.ts │ ├── package.json │ ├── repl/ │ │ ├── compiler.ts │ │ ├── formatter.ts │ │ ├── linter.ts │ │ └── main.css │ ├── src/ │ │ ├── components/ │ │ │ ├── CompileMode.tsx │ │ │ ├── editor/ │ │ │ │ ├── index.tsx │ │ │ │ ├── monacoTabs.tsx │ │ │ │ └── setupSolid.ts │ │ │ ├── error.tsx │ │ │ ├── newTab.tsx │ │ │ ├── preview.tsx │ │ │ ├── repl.tsx │ │ │ └── ui/ │ │ │ ├── Button.tsx │ │ │ ├── Checkbox.tsx │ │ │ ├── IconButton.tsx │ │ │ ├── Input.tsx │ │ │ ├── Label.tsx │ │ │ └── Menu.tsx │ │ ├── dockview/ │ │ │ └── solid.tsx │ │ ├── hooks/ │ │ │ └── useZoom.ts │ │ ├── index.ts │ │ ├── repl.tsx │ │ └── types.d.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── unocss.config.ts ├── patches/ │ └── monaco-editor.patch ├── pnpm-workspace.yaml ├── scripts/ │ ├── oxlint-plugin-unocss.ts │ └── unocss-worker.ts ├── tsconfig.json └── uno.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules/ dist/ .DS_Store ================================================ FILE: .oxfmtrc.json ================================================ { "$schema": "./node_modules/oxfmt/configuration_schema.json", "arrowParens": "always", "htmlWhitespaceSensitivity": "ignore", "printWidth": 120, "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "all", "useTabs": false, "quoteProps": "consistent", "ignorePatterns": ["node_modules/", "dist/", "pnpm-lock.yaml"], "sortTailwindcss": { "config": "uno.config.js" } } ================================================ FILE: .oxlintrc.json ================================================ { "jsPlugins": ["./scripts/oxlint-plugin-unocss.ts"], "rules": { "unocss/valid-class": "error", "unocss/no-conflicting-classes": "error", "eslint/no-unassigned-vars": "off" } } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 SolidJS Core Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

Solid Playground

# Solid Template Explorer This is the source code of the [solid playground](https://playground.solidjs.com) website. Through it you can quickly discover what the solid compiler will generate from your JSX templates. There are 3 modes available: - DOM: The classic SPA generation mechanism - SSR: The server side generation mechanism - HYDRATION: The client side generation for hydration - UNIVERSAL: The client side generation for universal (custom renderer) ## Getting up and running This project is built using the [pnpm](https://pnpm.js.org/) package manager. Once you got it up and running you can follow these steps the have a fully working environement: ```bash # Clone the project $ git clone https://github.com/solidjs/solid-playground # cd into the project and install the dependencies $ cd solid-playground && pnpm i # Start the dev server, the address is available at http://localhost:5173 $ pnpm run dev # Build the project $ pnpm run build ``` ## Credits / Technologies used - [solid-js](https://github.com/solidjs/solid/): The view library - [@babel/standalone](https://babeljs.io/docs/en/babel-standalone): The in-browser compiler. Solid compiler relies on babel - [monaco](https://microsoft.github.io/monaco-editor/): The in-browser code editor. This is the code editor that powers VS Code - [Windi CSS](https://windicss.org/): The CSS framework - [vite](https://vitejs.dev/): The module bundler - [workbox](https://developers.google.com/web/tools/workbox): The service worker generator - [pnpm](https://pnpm.js.org/): The package manager - [lz-string](https://github.com/pieroxy/lz-string): The string compression algorithm used to share REPL ================================================ FILE: package.json ================================================ { "name": "solid-playground-restructured", "version": "1.0.0", "private": true, "description": "", "keywords": [], "author": "", "workspaces": [ "./packages/*" ], "type": "module", "scripts": { "start": "cd packages/playground && pnpm start", "build": "cd packages/playground && pnpm build", "dev": "cd packages/playground && pnpm dev", "format": "oxfmt .", "lint": "oxlint ." }, "devDependencies": { "@changesets/cli": "2.31.0", "@oxlint/plugins": "^1.61.0", "@unocss/preset-wind3": "^66.6.8", "jiti": "^2.6.1", "oxfmt": "^0.46.0", "oxlint": "^1.61.0", "synckit": "^0.11.12", "unocss": "^66.6.8" }, "packageManager": "pnpm@10.33.2", "pnpm": { "patchedDependencies": { "monaco-editor": "patches/monaco-editor.patch" } } } ================================================ FILE: packages/playground/index.html ================================================ Solid Playground
================================================ FILE: packages/playground/package.json ================================================ { "name": "solid-playground", "private": true, "type": "module", "scripts": { "build": "vite build", "start": "vite preview", "dev": "vite", "tsc": "tsc" }, "dependencies": { "@solid-primitives/scheduled": "^1.5.3", "@solidjs/router": "^0.16.1", "dedent": "^1.7.2", "solid-dismiss": "^1.8.2", "solid-heroicons": "^3.2.4", "solid-js": "1.9.12", "solid-repl": "workspace:*" }, "devDependencies": { "@amoutonbrady/lz-string": "^0.1.0", "@babel/core": "^7.29.0", "@babel/plugin-syntax-jsx": "^7.28.6", "@babel/preset-typescript": "^7.28.5", "@babel/types": "^7.29.0", "@solidjs/router": "^0.16.1", "@types/babel__standalone": "^7.1.9", "@types/dedent": "^0.7.2", "assert": "^2.1.0", "csstype": "^3.2.3", "jszip": "^3.10.1", "monaco-editor": "^0.55.1", "register-service-worker": "^1.7.2", "typescript": "^6.0.3", "unocss": "^66.6.8", "vite": "^8.0.10", "vite-plugin-solid": "^2.11.12" } } ================================================ FILE: packages/playground/public/_redirects ================================================ /* /index.html 200 ================================================ FILE: packages/playground/public/manifest.webmanifest ================================================ { "dir": "ltr", "lang": "en", "name": "Solid REPL", "short_name": "Solid REPL", "scope": "/", "display": "standalone", "start_url": "/", "background_color": "transparent", "theme_color": "transparent", "description": "Solid REPL", "orientation": "any", "prefer_related_applications": false, "icons": [ { "src": "/square_logo.png", "sizes": "144x144", "type": "image/png" } ] } ================================================ FILE: packages/playground/public/robots.txt ================================================ User-agent: * Allow: / ================================================ FILE: packages/playground/public/sw.js ================================================ const cacheName = 'my-cache'; async function notifyClient(event) { const client = event.clientId ? await clients.get(event.clientId) : null; if (client) { client.postMessage({ type: 'cache' }); return; } const all = await clients.matchAll(); for (const c of all) c.postMessage({ type: 'cache' }); } function responsesDiffer(cached, fresh) { const cachedTag = cached.headers.get('etag') || cached.headers.get('last-modified'); const freshTag = fresh.headers.get('etag') || fresh.headers.get('last-modified'); if (cachedTag && freshTag) return cachedTag !== freshTag; return false; } async function fetchAndCache(cache, event) { try { const response = await fetch(event.request); if (response.ok) { await cache.put(event.request, response.clone()); } return response; } catch (e) { console.error(e); if (event.request.mode === 'navigate') { return await cache.match('/index.html'); } throw e; } } async function fetchWithCache(event) { const cache = await caches.open(cacheName); const cached = await cache.match(event.request); const fresh = fetchAndCache(cache, event); if (cached) { fresh.then((response) => { if (response && responsesDiffer(cached, response)) { notifyClient(event); } }); return cached; } return fresh; } function handleFetch(event) { if ( event.request.headers.get('cache-control') !== 'no-cache' && event.request.method === 'GET' && event.request.url.startsWith(self.location.origin) ) { event.respondWith(fetchWithCache(event)); } } self.addEventListener('fetch', handleFetch); self.addEventListener('install', () => { self.skipWaiting(); }); self.addEventListener('activate', (event) => { event.waitUntil( (async () => { const keys = await caches.keys(); await Promise.all(keys.filter((k) => k !== cacheName).map((k) => caches.delete(k))); await clients.claim(); })(), ); }); ================================================ FILE: packages/playground/src/app.tsx ================================================ import { Show, JSX, Suspense } from 'solid-js'; import { Route, Router } from '@solidjs/router'; import { eventBus, setEventBus } from './utils/serviceWorker'; import { Update } from './components/update'; import { useZoom } from 'solid-repl/src/hooks/useZoom'; import { Edit } from './pages/edit'; import { Home } from './pages/home'; import { Login } from './pages/login'; import { AppContextProvider } from './context'; export const App = (): JSX.Element => { /** * Those next three lines are useful to display a popup * if the client code has been updated. This trigger a signal * via an EventBus initiated in the service worker and * the couple line above. */ const { zoomState, updateZoom } = useZoom(); document.addEventListener('keydown', (e) => { if (!zoomState.overrideNative) return; if (!(e.ctrlKey || e.metaKey)) return; if (e.key === '=') { updateZoom('increase'); e.preventDefault(); } else if (e.key == '-') { updateZoom('decrease'); e.preventDefault(); } }); return (
( {props.children} )} > setEventBus(false)} />} />
); }; ================================================ FILE: packages/playground/src/components/header.tsx ================================================ import Dismiss from 'solid-dismiss'; import { A } from '@solidjs/router'; import { Icon } from 'solid-heroicons'; import { unwrap } from 'solid-js/store'; import { onCleanup, createSignal, Show, ParentComponent, children } from 'solid-js'; import { share, link, arrowDownTray, xCircle, bars_3, moon, sun } from 'solid-heroicons/outline'; import { exportToZip } from '../utils/exportFiles'; import { ZoomDropdown } from './zoomDropdown'; import { API, useAppContext } from '../context'; import { Button, LinkButton } from 'solid-repl/src/components/ui/Button'; import logo from '../assets/logo.svg?url'; export const Header: ParentComponent<{ compiler?: Worker; fork?: () => void; share: () => Promise; }> = (props) => { const [copy, setCopy] = createSignal(false); const context = useAppContext()!; const [showMenu, setShowMenu] = createSignal(false); const [showProfile, setShowProfile] = createSignal(false); const resolved = children(() => props.children); let menuBtnEl!: HTMLButtonElement; let profileBtn!: HTMLButtonElement; function shareLink() { props.share().then((url) => { navigator.clipboard.writeText(url).then(() => { setCopy(true); setTimeout(setCopy, 750, false); }); }); } window.addEventListener('resize', closeMobileMenu); onCleanup(() => { window.removeEventListener('resize', closeMobileMenu); }); function closeMobileMenu() { setShowMenu(false); } const menuButtonClasses = (show: boolean) => ({ 'rounded-none active:bg-gray-300 hover:bg-gray-300 dark:hover:text-black': show, }); return (
solid-js logo {resolved() || (

SolidJS Playground

)}
menuBtnEl} open={showMenu} setOpen={setShowMenu} show > ), outline: false, mini: false, }} /> Github
Login } > profileBtn} open={showProfile} setOpen={setShowProfile}>
{context.user()?.display}
); }; ================================================ FILE: packages/playground/src/components/setupSolid.ts ================================================ import { typescript } from 'monaco-editor'; const solidTypes: Record = import.meta.glob('/node_modules/{solid-js,csstype}/**/*.{d.ts,json}', { eager: true, query: '?raw', import: 'default', }); for (const path in solidTypes) { typescript.typescriptDefaults.addExtraLib(solidTypes[path], `file://${path}`); typescript.javascriptDefaults.addExtraLib(solidTypes[path], `file://${path}`); } import repl from 'solid-repl/src/repl'; export default repl; ================================================ FILE: packages/playground/src/components/update.tsx ================================================ import type { Component } from 'solid-js'; import { Portal } from 'solid-js/web'; import { Icon } from 'solid-heroicons'; import { xMark } from 'solid-heroicons/outline'; export const Update: Component<{ onDismiss: (...args: unknown[]) => unknown; }> = (props) => { const mount = document.getElementById('update'); return (

There's a new update available.

Refresh your browser or click the button below to get the latest update of the REPL.

); }; ================================================ FILE: packages/playground/src/components/zoomDropdown.tsx ================================================ import { Icon } from 'solid-heroicons'; import { magnifyingGlassPlus, minus, plus } from 'solid-heroicons/outline'; import Dismiss from 'solid-dismiss'; import { Component, createSignal, createEffect } from 'solid-js'; import { useZoom } from 'solid-repl/src/hooks/useZoom'; import { Button } from 'solid-repl/src/components/ui/Button'; import { Checkbox } from 'solid-repl/src/components/ui/Checkbox'; export const ZoomDropdown: Component<{ showMenu: boolean }> = (props) => { const [open, setOpen] = createSignal(false); const { zoomState, updateZoom, setZoomState } = useZoom(); const popupDuration = 1250; let containerEl!: HTMLDivElement; let prevZoom = zoomState.zoom; let timeoutId: number | null = null; let btnEl!: HTMLButtonElement; let prevFocusedEl: HTMLElement | null; let stealFocus = true; const onMouseMove = () => { stealFocus = true; window.clearTimeout(timeoutId!); }; const onKeyDownContainer = (e: KeyboardEvent) => { if (!open()) return; if (e.key === 'Escape' && !stealFocus) { if (prevFocusedEl) { setOpen(false); prevFocusedEl.focus(); stealFocus = true; } window.clearTimeout(timeoutId!); } if (!['Tab', 'Enter', 'Space'].includes(e.key)) return; stealFocus = false; prevFocusedEl = null; window.clearTimeout(timeoutId!); }; createEffect(() => { if (prevZoom === zoomState.zoom) return; prevZoom = zoomState.zoom; if (stealFocus) { prevFocusedEl = document.activeElement as HTMLElement; btnEl.focus(); stealFocus = false; } setOpen(true); window.clearTimeout(timeoutId!); timeoutId = window.setTimeout(() => { setOpen(false); stealFocus = true; if (prevFocusedEl) { prevFocusedEl.focus(); } }, popupDuration); }); createEffect(() => { if (!open()) { if (containerEl) { containerEl.removeEventListener('mouseenter', onMouseMove); } stealFocus = true; } else { if (containerEl) { containerEl.addEventListener('mouseenter', onMouseMove, { once: true }); } } }); return (
{ window.clearTimeout(timeoutId!); }} ref={containerEl} tabindex="-1" >
{zoomState.zoom}%
setZoomState('overrideNative', e.currentTarget.checked)} /> setZoomState('scaleIframe', e.currentTarget.checked)} />
); }; ================================================ FILE: packages/playground/src/context.tsx ================================================ import { Accessor, createContext, createResource, createSignal, ParentComponent, Resource, useContext } from 'solid-js'; import type { Tab } from 'solid-repl'; import { isDarkTheme } from './utils/isDarkTheme'; interface AppContextType { token: string; user: Resource<{ display: string; avatar: string } | undefined>; profile: Accessor; tabs: Accessor; setTabs: (x: Accessor | undefined) => void; dark: Accessor; toggleDark: () => void; } const AppContext = createContext(); // export const API = 'http://localhost:8787'; // export const API = '/api'; export const API = 'https://api.solidjs.com'; export const AppContextProvider: ParentComponent = (props) => { const [token, setToken] = createSignal(localStorage.getItem('token') || ''); const [user] = createResource(token, async (token) => { if (!token) return { display: '', avatar: '', }; const result = await fetch(`${API}/profile`, { headers: { authorization: `Bearer ${token}`, }, }); const body = await result.json(); return { display: body.display, avatar: body.avatar, }; }); const [dark, setDark] = createSignal(isDarkTheme()); document.body.classList.toggle('dark', dark()); let [tabsGetter, setTabs] = createSignal>(); return ( user()?.display || 'anonymous', tabs() { const tabs = tabsGetter(); if (!tabs) return undefined; return tabs(); }, setTabs(x) { setTabs(() => x); }, dark, toggleDark() { let x = !dark(); document.body.classList.toggle('dark', x); setDark(x); localStorage.setItem('dark', String(x)); }, }} > {props.children} ); }; export const useAppContext = () => useContext(AppContext); ================================================ FILE: packages/playground/src/index.tsx ================================================ import { render } from 'solid-js/web'; import { App } from './app'; import { registerServiceWorker } from './utils/serviceWorker'; import 'solid-repl/repl/main.css'; import 'virtual:uno.css'; render(() => , document.querySelector('#app')!); registerServiceWorker(); ================================================ FILE: packages/playground/src/pages/edit.tsx ================================================ import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'; import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; import CompilerWorker from 'solid-repl/repl/compiler?worker'; import FormatterWorker from 'solid-repl/repl/formatter?worker'; import LinterWorker from 'solid-repl/repl/linter?worker'; import { batch, createEffect, createResource, createSignal, lazy, onCleanup, Show, Suspense } from 'solid-js'; import { useLocation, useMatch, useNavigate, useParams, useSearchParams } from '@solidjs/router'; import { API, useAppContext } from '../context'; import { debounce } from '@solid-primitives/scheduled'; import { decompressFromURL } from '@amoutonbrady/lz-string'; import { defaultTabs } from 'solid-repl/src'; import type { Tab } from 'solid-repl'; import type { APIRepl } from './home'; import { Header } from '../components/header'; import { Button } from 'solid-repl/src/components/ui/Button'; function parseHash(hash: string, fallback: T): T { try { return JSON.parse(decompressFromURL(hash) || ''); } catch { return fallback; } } const Repl = lazy(() => import('../components/setupSolid')); window.MonacoEnvironment = { getWorker(_moduleId: unknown, label: string) { switch (label) { case 'css': return new cssWorker(); case 'json': return new jsonWorker(); case 'typescript': case 'javascript': return new tsWorker(); default: return new editorWorker(); } }, }; interface InternalTab extends Tab { _source: string; _name: string; } export const Edit = () => { const [searchParams] = useSearchParams(); const scratchpad = useMatch(() => '/'); const compiler = new CompilerWorker(); const formatter = new FormatterWorker(); const linter = new LinterWorker(); const params = useParams<{ user: string; repl: string }>(); const context = useAppContext()!; const navigate = useNavigate(); const location = useLocation(); let disableFetch: true | undefined; let readonly = () => !scratchpad() && context.profile() != params.user && !localStorage.getItem(params.repl); createEffect(() => { if (!scratchpad()) return; if (location.query.hash) { navigate(`/anonymous/${location.query.hash}`); } else if (location.hash) { const initialTabs = parseHash(location.hash.slice(1), defaultTabs); localStorage.setItem( 'scratchpad', JSON.stringify({ files: initialTabs.map((x) => ({ name: x.name, content: x.source })), }), ); navigate('/', { replace: true }); } }); const mapTabs = (toMap: (Tab | InternalTab)[]): InternalTab[] => toMap.map((tab) => { if ('_source' in tab) return tab; return { _name: tab.name, get name() { return this._name; }, set name(name: string) { this._name = name; updateRepl(); }, _source: tab.source, get source() { return this._source; }, set source(source: string) { this._source = source; updateRepl(); }, }; }); const [tabs, trueSetTabs] = createSignal([]); const setTabs = (tabs: (Tab | InternalTab)[]) => trueSetTabs(mapTabs(tabs)); context.setTabs(tabs); onCleanup(() => context.setTabs(undefined)); const [current, setCurrent] = createSignal(undefined, { equals: false }); const [resource, { mutate }] = createResource( () => ({ repl: params.repl, scratchpad: !!scratchpad() }), async ({ repl, scratchpad }): Promise => { if (disableFetch) { disableFetch = undefined; if (resource.latest) return resource.latest; } let output: APIRepl; if (scratchpad) { const myScratchpad = localStorage.getItem('scratchpad'); if (!myScratchpad) { output = { files: defaultTabs.map((x) => ({ name: x.name, content: x.source, })), } as APIRepl; localStorage.setItem('scratchpad', JSON.stringify(output)); } else { output = JSON.parse(myScratchpad); } } else { output = await fetch(`${API}/repl/${repl}`, { headers: { authorization: context.token ? `Bearer ${context.token}` : '' }, }).then((r) => r.json()); } batch(() => { setTabs( output.files.map((x) => { return { name: x.name, source: x.content }; }), ); setCurrent(output.files[0].name); }); return output; }, ); const reset = () => { batch(() => { setTabs(mapTabs(defaultTabs)); setCurrent(defaultTabs[0].name); }); }; const publishScratchpad = async (title: string) => { const newRepl = { title, public: true, labels: [] as string[], version: '1.0', files: tabs().map((x) => ({ name: x.name, content: x.source })), }; const response = await fetch(`${API}/repl`, { method: 'POST', headers: { 'authorization': context.token ? `Bearer ${context.token}` : '', 'Content-Type': 'application/json', }, body: JSON.stringify(newRepl), }); if (response.status >= 400) { throw new Error(response.statusText); } const { id, write_token } = await response.json(); if (write_token) { localStorage.setItem(id, write_token); const repls = localStorage.getItem('repls'); if (repls) { localStorage.setItem('repls', JSON.stringify([...JSON.parse(repls), id])); } else { localStorage.setItem('repls', JSON.stringify([id])); } } mutate(() => ({ id, title: newRepl.title, labels: newRepl.labels, files: newRepl.files, version: newRepl.version, public: newRepl.public, size: 0, created_at: '', })); const url = `/${context.profile()}/${id}`; disableFetch = true; navigate(url); return url; }; const [forkPromptFor, setForkPromptFor] = createSignal(null); const [forkDeclinedFor, setForkDeclinedFor] = createSignal(null); const forkPromptOpen = () => forkPromptFor() === params.repl; const forkDeclined = () => forkDeclinedFor() === params.repl; const onUserEdit = () => { if (!readonly() || forkDeclined()) return; setForkPromptFor(params.repl); }; const updateRepl = debounce( () => { if (readonly()) return; const files = tabs().map((x) => ({ name: x.name, content: x.source })); if (scratchpad()) { localStorage.setItem('scratchpad', JSON.stringify({ files })); } const repl = resource.latest; if (!repl) return; const loggedIn = context.token && params.user && context.profile() == params.user; if (loggedIn || localStorage.getItem(params.repl)) { fetch(`${API}/repl/${params.repl}`, { method: 'PUT', headers: { 'authorization': context.token ? `Bearer ${context.token}` : '', 'Content-Type': 'application/json', }, body: JSON.stringify({ ...(localStorage.getItem(params.repl) ? { write_token: localStorage.getItem(params.repl) } : {}), title: repl.title, version: repl.version, public: repl.public, labels: repl.labels, files, }), }); } }, !!scratchpad() ? 10 : 1000, ); return ( <>
{}} share={async () => { if (scratchpad()) { const url = await publishScratchpad(`${context.user()?.display || 'Anonymous'}'s Scratchpad`); return `${window.location.origin}${url}`; } else if (readonly()) { const original = resource.latest; const url = await publishScratchpad(original?.title ? `${original.title} (fork)` : 'Forked Repl'); return `${window.location.origin}${url}`; } else { return window.location.href; } }} > { if (e.key === 'Enter') e.currentTarget.blur(); }} onChange={(e) => { const title = e.currentTarget.value; if (scratchpad() || readonly()) { if (title) publishScratchpad(title); } else { mutate((x) => x && { ...x, title }); updateRepl(); } }} />
} >
); }; ================================================ FILE: packages/playground/src/pages/home.tsx ================================================ import { A, useParams } from '@solidjs/router'; import { Icon } from 'solid-heroicons'; import { eye, eyeSlash, plus, xMark } from 'solid-heroicons/outline'; import { createResource, createSignal, For, Show, Suspense } from 'solid-js'; import { createStore, produce } from 'solid-js/store'; import { API, useAppContext } from '../context'; import { Header } from '../components/header'; import { timeAgo } from '../utils/date'; import { Button } from 'solid-repl/src/components/ui/Button'; interface ReplFile { name: string; content: string; } export interface APIRepl { id: string; title: string; labels: string[]; files: ReplFile[]; version: string; public: boolean; size: number; created_at: string; updated_at?: string; } interface Repls { total: number; list: APIRepl[]; } export const Home = () => { const params = useParams(); const context = useAppContext()!; const [repls, setRepls] = createStore({ total: 0, list: [] }); const [resourceRepls] = createResource( () => ({ user: params.user }), async ({ user }) => { if (!user && !context.token) return { total: 0, list: [] }; let output = await fetch(`${API}/repl${user ? `/${user}/list` : '?'}`, { headers: { Authorization: `Bearer ${context.token}`, }, }).then((r) => r.json()); setRepls(output); return output; }, ); const get = (x: T) => { resourceRepls(); return x; }; const [open, setOpen] = createSignal(); return ( <>
{ const url = new URL(document.location.origin); url.pathname = `/${params.user || context.profile()}`; return url.toString(); }} />
{`${params.user}'s`} Repls} > } > {(repl, i) => ( { if (e.target.tagName !== 'A') e.currentTarget.querySelector('a')!.click(); }} > )}
Title Edited Options
{repl.title} {timeAgo(Date.now() - new Date(repl.updated_at || repl.created_at).getTime())} { e.stopPropagation(); fetch(`${API}/repl/${repl.id}`, { method: 'PATCH', headers: { 'authorization': `Bearer ${context.token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ public: !repl.public, }), }); setRepls( produce((x) => { x!.list[i()].public = !repl.public; }), ); }} /> { e.stopPropagation(); setOpen(repl.id); }} />
{ if (e.target !== e.currentTarget) return; setOpen(undefined); }} role="presentation" >
); }; ================================================ FILE: packages/playground/src/pages/login.tsx ================================================ import { Navigate, useSearchParams } from '@solidjs/router'; import { useAppContext } from '../context'; export const Login = () => { const [params] = useSearchParams(); const context = useAppContext()!; context.token = `${params.token ?? ''}`; return ; }; ================================================ FILE: packages/playground/src/utils/date.ts ================================================ const formatter = new Intl.RelativeTimeFormat('en'); export const timeAgo = (ms: number): string => { const sec = Math.round(ms / 1000); const min = Math.round(sec / 60); const hr = Math.round(min / 60); const day = Math.round(hr / 24); const month = Math.round(day / 30); const year = Math.round(month / 12); if (sec < 10) { return 'just now'; } else if (sec < 45) { return formatter.format(-sec, 'second'); } else if (sec < 90 || min < 45) { return formatter.format(-min, 'minute'); } else if (min < 90 || hr < 24) { return formatter.format(-hr, 'hour'); } else if (hr < 36 || day < 30) { return formatter.format(-day, 'day'); } else if (month < 18) { return formatter.format(-month, 'month'); } else { return formatter.format(-year, 'year'); } }; ================================================ FILE: packages/playground/src/utils/exportFiles.tsx ================================================ import pkg from '../../package.json'; import type { Tab } from 'solid-repl'; import dedent from 'dedent'; const viteConfigFile = dedent` import { defineConfig } from "vite"; import solidPlugin from "vite-plugin-solid"; export default defineConfig({ plugins: [solidPlugin()], build: { target: "esnext", polyfillDynamicImport: false, }, }); `; const tsConfig = JSON.stringify( { compilerOptions: { strict: true, module: 'ESNext', target: 'ESNext', jsx: 'preserve', esModuleInterop: true, sourceMap: true, allowJs: true, lib: ['es6', 'dom'], rootDir: 'src', moduleResolution: 'node', jsxImportSource: 'solid-js', types: ['solid-js', 'solid-js/dom'], }, }, null, 2, ); const indexHTML = (tabs: Tab[]) => dedent` Vite Sandbox
`; /** * This function will calculate the dependencies of the * package.json by using the imports list provided by the bundler, * and then generating the package.json itself, for the export */ function packageJSON(imports: string[]): string { const deps = imports.reduce( (acc, importPath): Record => { const name = importPath.split('/')[0]; if (!acc[name]) acc[name] = '*'; return acc; }, {} as Record, ); return JSON.stringify( { scripts: { start: 'vite', build: 'vite build', }, dependencies: deps, devDependencies: { 'vite': pkg.devDependencies['vite'], 'vite-plugin-solid': pkg.devDependencies['vite-plugin-solid'], }, }, null, 2, ); } /** * This function will convert the tabs of the playground * into a ZIP formatted playground that can then be reimported later on */ export async function exportToZip(tabs: Tab[]): Promise { const { default: JSZip } = await import('jszip'); const zip = new JSZip(); // basic structure zip.file('index.html', indexHTML(tabs)); zip.file('vite.config.ts', viteConfigFile); zip.file('tsconfig.json', tsConfig); zip.folder('src'); for (const tab of tabs) { if (tab.name == 'import_map.json') { zip.file('package.json', packageJSON(Object.keys(JSON.parse(tab.source)))); } else { zip.file(`src/${tab.name}`, tab.source); } } const blob = await zip.generateAsync({ type: 'blob' }); const url = URL.createObjectURL(blob); const anchor = () as HTMLElement; document.body.prepend(anchor); anchor.click(); anchor.remove(); } ================================================ FILE: packages/playground/src/utils/isDarkTheme.ts ================================================ export const isDarkTheme = () => { if (typeof window !== 'undefined') { if (window.localStorage) { const isDarkTheme = window.localStorage.getItem('dark'); if (typeof isDarkTheme === 'string') { return isDarkTheme === 'true'; } } const userMedia = window.matchMedia('(prefers-color-scheme: dark)'); if (userMedia.matches) { return true; } } // Default theme is light. return false; }; ================================================ FILE: packages/playground/src/utils/serviceWorker.ts ================================================ import { register } from 'register-service-worker'; import { createSignal } from 'solid-js'; const [eventBus, setEventBus] = createSignal(); function registerServiceWorker(): void { if ('serviceWorker' in navigator && import.meta.env.PROD) { window.addEventListener('load', () => { register('/sw.js', { updated() { setEventBus(true); }, }); }); } } if (import.meta.env.PROD) { navigator.serviceWorker?.addEventListener('message', (event) => { if (event.data.type == 'cache') { setEventBus(true); } }); } export { eventBus, setEventBus, registerServiceWorker }; ================================================ FILE: packages/playground/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "lib": ["ESNext", "DOM", "DOM.Iterable", "WebWorker"], "types": ["vite/client"] }, "include": ["./src/**/*"], "exclude": ["node_modules/"] } ================================================ FILE: packages/playground/unocss.config.ts ================================================ import { theme } from '@unocss/preset-wind3'; import { defineConfig } from 'unocss'; import sharedConfig from '../../uno.config.ts'; export default defineConfig({ ...sharedConfig, theme: { ...(sharedConfig as any).theme, fontFamily: { sans: 'Gordita, ' + theme.fontFamily!.sans, }, }, content: { filesystem: ['./src/**/*.tsx', '../solid-repl/src/**/*.{tsx,ts}'], }, }); ================================================ FILE: packages/playground/vite.config.ts ================================================ import { defineConfig } from 'vite'; import solidPlugin from 'vite-plugin-solid'; import UnoCSS from 'unocss/vite'; export default defineConfig((env) => ({ plugins: [solidPlugin(), UnoCSS()], define: { 'process.env.NODE_DEBUG': 'false', ...(env.command == 'build' ? {} : { global: 'globalThis' }), }, build: { target: 'esnext', rolldownOptions: { output: { entryFileNames: `assets/[name].js`, chunkFileNames: `assets/[name].js`, assetFileNames: `assets/[name].[ext]`, }, }, }, worker: { rolldownOptions: { output: { entryFileNames: `assets/[name].js`, }, }, }, server: { proxy: { '/api': { target: 'http://localhost:8787', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), }, }, }, })); ================================================ FILE: packages/solid-repl/build.ts ================================================ import { build } from 'esbuild'; import { readFileSync, writeFileSync, unlinkSync } from 'fs'; import { copyFileSync } from 'fs-extra'; build({ entryPoints: ['./repl/compiler.ts', './repl/formatter.ts', './repl/linter.ts', './repl/main.css'], outdir: './dist', minify: true, bundle: true, external: ['/Gordita-Medium.woff', '/Gordita-Regular.woff', '/Gordita-Bold.woff'], define: { 'process.env.NODE_DEBUG': 'false', 'preventAssignment': 'true', }, }).then(() => { const unoCSS_build = readFileSync('./uno.css'); const generated_bundle = readFileSync('./dist/main.css'); const output_bundle = Buffer.concat([generated_bundle, unoCSS_build]); writeFileSync('./dist/bundle.css', output_bundle); unlinkSync('./uno.css'); unlinkSync('./dist/main.css'); copyFileSync('./src/types.d.ts', './dist/types.d.ts'); }); ================================================ FILE: packages/solid-repl/package.json ================================================ { "name": "solid-repl", "version": "0.26.0", "description": "Quickly discover what the solid compiler will generate from your JSX template", "homepage": "https://playground.solidjs.com", "author": "Alexandre Mouton-Brady", "repository": { "type": "git", "url": "https://github.com/solidjs/solid-playground.git" }, "files": [ "dist" ], "module": "dist/repl.jsx", "types": "src/types.d.ts", "scripts": { "build": "tsc -p tsconfig.build.json && unocss \"./src/**\" && jiti build.ts", "tsc": "tsc" }, "dependencies": { "@floating-ui/dom": "^1.7.6", "@shikijs/monaco": "^4.0.2", "@solid-primitives/media": "^2.3.5", "@solid-primitives/scheduled": "^1.5.3", "dedent": "^1.7.2", "dockview-core": "^5.2.0", "shiki": "^4.0.2", "solid-dismiss": "^1.8.2", "solid-floating-ui": "^0.3.1", "solid-heroicons": "^3.2.4" }, "devDependencies": { "@babel/core": "^7.29.0", "@babel/plugin-syntax-jsx": "^7.28.6", "@babel/preset-typescript": "^7.28.5", "@babel/standalone": "^7.29.2", "@babel/types": "^7.29.0", "@shikijs/langs": "^4.0.2", "@shikijs/themes": "^4.0.2", "@types/babel__standalone": "^7.1.9", "@types/dedent": "^0.7.2", "@types/fs-extra": "^11.0.4", "@unocss/cli": "^66.6.8", "@unocss/preset-wind3": "^66.6.8", "@unocss/reset": "^66.6.8", "babel-preset-solid": "^1.9.12", "esbuild": "^0.28.0", "eslint-solid-standalone": "<0.14.0", "fs-extra": "^11.3.4", "jiti": "^2.6.1", "monaco-editor": "^0.55.1", "prettier": "^3.8.3", "solid-js": "1.9.12", "typescript": "^6.0.3", "unocss": "^66.6.8" }, "peerDependencies": { "solid-js": ">=1.7.0" } } ================================================ FILE: packages/solid-repl/repl/compiler.ts ================================================ import type { Tab } from 'solid-repl'; import { transform } from '@babel/standalone'; // @ts-ignore import babelPresetSolid from 'babel-preset-solid'; import dd from 'dedent'; function uid(str: string) { return Array.from(str) .reduce((s, c) => (Math.imul(31, s) + c.charCodeAt(0)) | 0, 0) .toString(); } function babelTransform(filename: string, code: string, externals: Record) { const handleImportee = (node: { value: string } | null | undefined) => { if (!node || typeof node.value !== 'string') return; const importee = node.value; if (importee.startsWith('.')) { node.value = 'solidrepl:' + importee; } else if (!importee.includes('://')) { if (!(importee in externals)) externals[importee] = `https://esm.sh/${importee}`; } }; let { code: transformedCode } = transform(code, { plugins: [ function importRewriter() { return { visitor: { Import(path: any) { handleImportee(path.parent.arguments[0]); }, ImportDeclaration(path: any) { handleImportee(path.node.source); }, ExportAllDeclaration(path: any) { handleImportee(path.node.source); }, ExportNamedDeclaration(path: any) { handleImportee(path.node.source); }, }, }; }, ], presets: [ [babelPresetSolid, { generate: 'dom', hydratable: false }], ['typescript', { onlyRemoveTypeImports: true }], ], filename: filename + '.tsx', }); return transformedCode!.replace('render(', 'window.dispose = render('); } function transformTab(tab: Tab, externals: Record): string { if (tab.name.endsWith('.css')) { const id = uid(tab.name); return dd` (() => { let stylesheet = document.getElementById('${id}'); if (!stylesheet) { stylesheet = document.createElement('style') stylesheet.setAttribute('id', '${id}') document.head.appendChild(stylesheet) } const styles = document.createTextNode(\`${tab.source.replace(/`/g, '\\`').replace(/\$\{/g, '\\${')}\`) stylesheet.innerHTML = '' stylesheet.appendChild(styles) })() `; } return babelTransform(tab.name, tab.source, externals); } function compile(tabs: Tab[], event: string) { const externals: Record = {}; const compiled: Record = {}; for (const tab of tabs) { const key = `./${tab.name.replace(/\.(tsx|jsx)$/, '')}`; compiled[key] = transformTab(tab, externals); } return { event, compiled, externals }; } function babel(tab: Tab, compileOpts: any) { const { code } = transform(tab.source, { presets: [ [babelPresetSolid, compileOpts], ['typescript', { onlyRemoveTypeImports: true }], ], filename: tab.name, }); return { event: 'BABEL', compiled: code }; } self.addEventListener('message', ({ data }) => { const { event, tabs, tab, compileOpts } = data; try { if (event === 'BABEL') { self.postMessage(babel(tab, compileOpts)); } else if (event === 'ROLLUP') { self.postMessage(compile(tabs, event)); } } catch (e) { self.postMessage({ event: 'ERROR', error: e }); } }); export {}; ================================================ FILE: packages/solid-repl/repl/formatter.ts ================================================ import { format as prettierFormat } from 'prettier/standalone'; import * as prettierPluginBabel from 'prettier/plugins/babel'; import * as prettierPluginEstree from 'prettier/plugins/estree'; function format(code: string) { return prettierFormat(code, { parser: 'babel-ts', plugins: [prettierPluginBabel, prettierPluginEstree as any], }); } self.addEventListener('message', async ({ data }) => { const { event, code } = data; switch (event) { case 'FORMAT': self.postMessage({ event: 'FORMAT', code: await format(code), }); break; } }); export {}; ================================================ FILE: packages/solid-repl/repl/linter.ts ================================================ import { verify, verifyAndFix } from 'eslint-solid-standalone'; import type { Linter } from 'eslint-solid-standalone'; import type { editor } from 'monaco-editor'; export interface LinterWorkerPayload { event: 'LINT' | 'FIX'; code: string; ruleSeverityOverrides?: Record; } const messagesToMarkers = (messages: Array): Array => { if (messages.some((m) => m.fatal)) return []; // no need for any extra highlights on parse errors return messages.map((m) => ({ startLineNumber: m.line, endLineNumber: m.endLine ?? m.line, startColumn: m.column, endColumn: m.endColumn ?? m.column, message: `${m.message}\neslint(${m.ruleId})`, severity: m.severity === 2 ? 8 /* error */ : 4 /* warning */, })); }; self.addEventListener('message', ({ data }: MessageEvent) => { const { event } = data; try { if (event === 'LINT') { const { code, ruleSeverityOverrides } = data; self.postMessage({ event: 'LINT' as const, markers: messagesToMarkers(verify(code, ruleSeverityOverrides)), }); } else if (event === 'FIX') { const { code, ruleSeverityOverrides } = data; const fixReport = verifyAndFix(code, ruleSeverityOverrides); self.postMessage({ event: 'FIX' as const, markers: messagesToMarkers(fixReport.messages), output: fixReport.output, fixed: fixReport.fixed, }); } } catch (e) { console.error(e); self.postMessage({ event: 'ERROR' as const, error: e }); } }); ================================================ FILE: packages/solid-repl/repl/main.css ================================================ @import url('@unocss/reset/tailwind.css'); @font-face { font-family: 'Gordita'; src: url('/Gordita-Regular.woff') format('woff'); font-weight: 400; font-style: normal; } @font-face { font-family: 'Gordita'; src: url('/Gordita-Bold.woff') format('woff'); font-weight: 700; font-style: normal; } @font-face { font-family: 'Gordita'; src: url('/Gordita-Medium.woff') format('woff'); font-weight: 500; font-style: normal; } div[contenteditable='true']:focus { outline: none !important; } .dark { color-scheme: dark; } textarea.monaco-mouse-cursor-text:focus { box-shadow: unset; } #app .dockview-theme-abyss-spaced { padding-top: 0; --dv-color-abyss-dark: theme('colors.neutral.100'); --dv-color-abyss: #ffffff; --dv-color-abyss-light: theme('colors.neutral.100'); --dv-color-abyss-lighter: theme('colors.neutral.100'); --dv-color-abyss-accent: theme('colors.medium'); --dv-color-abyss-primary-text: theme('colors.neutral.800'); --dv-color-abyss-secondary-text: theme('colors.neutral.400'); } .dark #app .dockview-theme-abyss-spaced { --dv-color-abyss-dark: theme('colors.neutral.950'); --dv-color-abyss: theme('colors.neutral.900'); --dv-color-abyss-light: theme('colors.neutral.800/50'); --dv-color-abyss-lighter: theme('colors.neutral.800/50'); --dv-color-abyss-accent: theme('colors.medium'); --dv-color-abyss-primary-text: theme('colors.neutral.100'); --dv-color-abyss-secondary-text: theme('colors.neutral.400'); } #app .dockview-theme-abyss-spaced .dv-groupview.dv-active-group { border: 2px solid var(--dv-color-abyss-accent); } #app .dockview-theme-abyss-spaced .dv-groupview.dv-inactive-group { border: 2px solid transparent; } ================================================ FILE: packages/solid-repl/src/components/CompileMode.tsx ================================================ import { Component, Setter } from 'solid-js'; import { Label } from './ui/Label'; import { Input } from './ui/Input'; export const compileOptions = { SSR: { generate: 'ssr', hydratable: true }, DOM: { generate: 'dom', hydratable: false }, HYDRATABLE: { generate: 'dom', hydratable: true }, UNIVERSAL: { generate: 'universal', hydratable: false, moduleName: 'solid-universal-module' as string, }, } as const; interface CompileModeProps { mode: (typeof compileOptions)[keyof typeof compileOptions]; setMode: Setter<(typeof compileOptions)[keyof typeof compileOptions]>; universalModuleName: string; setUniversalModuleName: Setter; } export const CompileMode: Component = (props) => { return (
{(['DOM', 'SSR', 'HYDRATABLE'] as const).map((m) => ( ))}
); }; ================================================ FILE: packages/solid-repl/src/components/editor/index.tsx ================================================ import { Component, createEffect, onMount, onCleanup, Show } from 'solid-js'; import { Uri, languages, editor as mEditor, KeyMod, KeyCode, typescript } from 'monaco-editor'; import { useZoom } from '../../hooks/useZoom'; import { throttle } from '@solid-primitives/scheduled'; import { bell, bellSlash, codeBracket } from 'solid-heroicons/outline'; import { register } from './setupSolid'; import { IconButton } from '../ui/IconButton'; const Editor: Component<{ model: mEditor.ITextModel; disabled?: true; isDark?: boolean; withMinimap?: boolean; formatter?: Worker; linter?: Worker; displayErrors?: boolean; setDisplayErrors?: (value: boolean) => void; onDocChange?: (code: string) => void; onUserEdit?: () => void; onEditorReady?: ( editor: mEditor.IStandaloneCodeEditor, monaco: { Uri: typeof Uri; editor: typeof mEditor; }, ) => void; }> = (props) => { let parent!: HTMLDivElement; let editor: mEditor.IStandaloneCodeEditor; const { zoomState } = useZoom(); if (props.formatter) { languages.registerDocumentFormattingEditProvider('typescript', { async provideDocumentFormattingEdits(model) { props.formatter!.postMessage({ event: 'FORMAT', code: model.getValue(), pos: editor.getPosition(), }); return new Promise((resolve) => { props.formatter!.addEventListener( 'message', ({ data: { code } }) => { resolve([ { range: model.getFullModelRange(), text: code, }, ]); }, { once: true }, ); }); }, }); } if (props.linter) { const listener = ({ data }: any) => { if (props.displayErrors) { const { event } = data; if (event === 'LINT') { mEditor.setModelMarkers(props.model, 'eslint', data.markers); } else if (event === 'FIX') { mEditor.setModelMarkers(props.model, 'eslint', data.markers); data.fixed && props.model.setValue(data.output); } } }; props.linter.addEventListener('message', listener); onCleanup(() => props.linter?.removeEventListener('message', listener)); } const runLinter = throttle((code: string) => { if (props.linter && props.displayErrors) { props.linter.postMessage({ event: 'LINT', code, }); } }, 250); // Initialize Monaco onMount(() => { editor = mEditor.create(parent, { model: null, fontFamily: 'Menlo, Monaco, "Courier New", monospace', automaticLayout: true, readOnly: props.disabled, fontSize: zoomState.fontSize, lineDecorationsWidth: 5, lineNumbersMinChars: 3, padding: { top: 15 }, minimap: { enabled: props.withMinimap, }, dropIntoEditor: { enabled: false, }, }); createEffect(() => { editor.updateOptions({ readOnly: !!props.disabled }); }); if (props.linter) { editor.addAction({ id: 'eslint.executeAutofix', label: 'Fix all auto-fixable problems', contextMenuGroupId: '1_modification', contextMenuOrder: 3.5, run: (ed) => { const code = ed.getValue(); if (code) { props.linter?.postMessage({ event: 'FIX', code, }); } }, }); } editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyS, () => { // auto-format editor.getAction('editor.action.formatDocument')?.run(); // auto-fix problems props.displayErrors && editor.getAction('eslint.executeAutofix')?.run(); editor.focus(); }); editor.onDidChangeModelContent((e) => { const code = editor.getValue(); props.onDocChange?.(code); runLinter(code); if (!e.isFlush) props.onUserEdit?.(); }); }); onCleanup(() => editor.dispose()); createEffect(() => { editor.setModel(props.model); register(); }); createEffect(() => { mEditor.setTheme(props.isDark ? 'dark-plus' : 'light-plus'); }); createEffect(() => { const fontSize = zoomState.fontSize; editor.updateOptions({ fontSize }); }); createEffect(() => { if (props.disabled) return; typescript.typescriptDefaults.setDiagnosticsOptions({ noSemanticValidation: !props.displayErrors, noSyntaxValidation: !props.displayErrors, }); }); createEffect(() => { if (props.displayErrors) { // run on mount and when displayLintMessages is turned on runLinter(editor.getValue()); } else { // reset eslint markers when displayLintMessages is turned off mEditor.setModelMarkers(props.model, 'eslint', []); } }); onMount(() => { props.onEditorReady?.(editor, { Uri, editor: mEditor }); }); return ( <>
props.setDisplayErrors?.(!props.displayErrors)} /> editor.getAction('editor.action.formatDocument')?.run()} /> TypeScript
); }; export default Editor; ================================================ FILE: packages/solid-repl/src/components/editor/monacoTabs.tsx ================================================ import { createMemo, onCleanup } from 'solid-js'; import type { Tab } from 'solid-repl'; import { Uri, editor, IDisposable } from 'monaco-editor'; export const createMonacoTabs = (folder: string, tabs: () => Tab[]) => { const currentTabs = createMemo>((prevTabs) => { const newTabs = new Map(); for (const tab of tabs()) { const url = `file:///${folder}/${tab.name}`; const lookup = prevTabs?.get(url); if (!lookup) { const uri = Uri.parse(url); const model = editor.createModel(tab.source, undefined, uri); const watcher = model.onDidChangeContent(() => (tab.source = model.getValue())); newTabs.set(url, { model, watcher }); } else { lookup.model.setValue(tab.source); lookup.watcher.dispose(); lookup.watcher = lookup.model.onDidChangeContent(() => (tab.source = lookup.model.getValue())); newTabs.set(url, lookup); } } if (prevTabs) { for (const [old, lookup] of prevTabs) { if (!newTabs.has(old)) lookup.model.dispose(); } } return newTabs; }); onCleanup(() => { for (const lookup of currentTabs().values()) lookup.model.dispose(); }); return currentTabs; }; ================================================ FILE: packages/solid-repl/src/components/editor/setupSolid.ts ================================================ import { languages, editor, typescript } from 'monaco-editor'; import { shikiToMonaco } from '@shikijs/monaco'; import { createHighlighterCoreSync } from 'shiki/core'; import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'; import darkPlus from '@shikijs/themes/dark-plus'; import lightPlus from '@shikijs/themes/light-plus'; import tsx from '@shikijs/langs/tsx'; import css from '@shikijs/langs/css'; import html from '@shikijs/langs/html'; import json from '@shikijs/langs/json'; const jsEngine = createJavaScriptRegexEngine(); const compilerOptions: typescript.CompilerOptions = { strict: true, target: typescript.ScriptTarget.ESNext, module: typescript.ModuleKind.ESNext, moduleResolution: typescript.ModuleResolutionKind.NodeJs, jsx: typescript.JsxEmit.Preserve, jsxImportSource: 'solid-js', allowNonTsExtensions: true, }; typescript.typescriptDefaults.setCompilerOptions(compilerOptions); typescript.javascriptDefaults.setCompilerOptions(compilerOptions); const loader = createHighlighterCoreSync({ themes: [darkPlus, lightPlus], langs: [tsx, css, html, json], engine: jsEngine, }); languages.register({ id: 'tsx' }); languages.register({ id: 'css' }); languages.register({ id: 'html' }); languages.register({ id: 'json' }); export function register() { shikiToMonaco(loader, { editor, languages: { ...languages, setTokensProvider: (id: string, provider: any) => { if (id === 'tsx') { languages.setTokensProvider('tsx', provider); languages.setTokensProvider('typescript', provider); languages.setTokensProvider('javascript', provider); } else languages.setTokensProvider(id, provider); }, } as any, }); } register(); ================================================ FILE: packages/solid-repl/src/components/error.tsx ================================================ import { Component, createMemo, createSignal } from 'solid-js'; import { Icon } from 'solid-heroicons'; import { chevronDown, chevronRight, xMark } from 'solid-heroicons/solid'; import { IconButton } from './ui/IconButton'; export const Error: Component<{ onDismiss: (...args: unknown[]) => unknown; message: string; }> = (props) => { const lines = createMemo(() => props.message.split('\n')); const firstLine = () => lines()[0] ?? ''; const stackTrace = () => lines().slice(1).join('\n'); const [isOpen, setIsOpen] = createSignal(false); return (
setIsOpen(event.currentTarget.open)}>
          
        
props.onDismiss()} />
); }; ================================================ FILE: packages/solid-repl/src/components/newTab.tsx ================================================ import { Component, createSignal, For, createMemo, onMount, Show } from 'solid-js'; import { Icon } from 'solid-heroicons'; import { magnifyingGlass, documentPlus, document as documentIcon, square_2Stack, chevronRight, arrowUpTray, ellipsisHorizontal, } from 'solid-heroicons/outline'; import type { Tab } from 'solid-repl'; import { Input } from './ui/Input'; import { IconButton } from './ui/IconButton'; import { Label } from './ui/Label'; import { Menu, MenuItem } from './ui/Menu'; import { pencil, trash as trashIcon } from 'solid-heroicons/outline'; import Dismiss from 'solid-dismiss'; interface NewTabProps { tabs: Tab[]; onOpenPane: (id: string) => void; onOpenFile: (name: string) => void; onNewFile: (name: string) => void; onUpload: (name: string, source: string) => void; onDeleteFile: (name: string) => void; onRenameFile: (oldName: string, newName: string) => void; onClose: () => void; } export const NewTab: Component = (props) => { const [query, setQuery] = createSignal(''); const [selectedIndex, setSelectedIndex] = createSignal(0); const [renamingFile, setRenamingFile] = createSignal(null); let inputRef!: HTMLInputElement; let fileInputRef!: HTMLInputElement; const categories = createMemo(() => { const q = query().toLowerCase(); const sections: { title: string; items: any[] }[] = []; let count = 0; // Panes - Always show Preview and Output const paneItems = ['Preview', 'Output'] .filter((p) => p.toLowerCase().includes(q)) .map((p) => ({ type: 'pane', id: p, label: p, icon: square_2Stack, globalIndex: count++, })); if (paneItems.length) { sections.push({ title: 'Panes', items: paneItems }); } // Files const files = props.tabs.filter((t) => t.name.toLowerCase().includes(q)); if (files.length > 0) { sections.push({ title: 'Files', items: files.map((t) => ({ type: 'file', id: t.name, label: t.name, icon: documentIcon, globalIndex: count++, })), }); } // Actions const actions = []; if (q === '' || 'upload file'.includes(q)) { actions.push({ type: 'action', id: 'upload', label: 'Upload File', icon: arrowUpTray, }); } if (q !== '' && !props.tabs.some((t) => t.name.toLowerCase() === q)) { actions.push({ type: 'new', id: q, label: `Create "${query()}"`, icon: documentPlus, }); } if (actions.length > 0) { sections.push({ title: 'Actions', items: actions.map((item) => ({ ...item, globalIndex: count++ })), }); } return sections; }); const allItems = createMemo(() => categories().flatMap((c) => c.items)); const handleKeyDown = (e: KeyboardEvent) => { if (renamingFile()) return; const items = allItems(); if (e.key === 'ArrowDown') { setSelectedIndex((i) => (i + 1) % items.length); } else if (e.key === 'ArrowUp') { setSelectedIndex((i) => (i - 1 + items.length) % items.length); } else if (e.key === 'Enter') { const selected = items[selectedIndex()]; if (selected) { handleSelect(selected); } } }; const handleSelect = (item: any) => { props.onClose(); if (item.type === 'pane') { props.onOpenPane(item.id); } else if (item.type === 'file') { props.onOpenFile(item.id); } else if (item.type === 'new') { props.onNewFile(item.id); } else if (item.type === 'action') { if (item.id === 'upload') { fileInputRef.click(); } } }; const handleFileUpload = (e: Event) => { const file = (e.target as HTMLInputElement).files?.[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { const content = e.target?.result as string; props.onUpload(file.name, content); props.onClose(); }; reader.readAsText(file); } }; const [activeMenu, setActiveMenu] = createSignal(null); onMount(() => requestAnimationFrame(() => inputRef.focus())); return (
{ setQuery(e.currentTarget.value); setSelectedIndex(0); }} onKeyDown={handleKeyDown} />
{(category) => (
{(item) => { let btnRef: HTMLButtonElement | undefined; const isActive = () => item.globalIndex === selectedIndex(); const isRenaming = () => renamingFile() === item.id; return (
!isRenaming() && handleSelect(item)} >
{item.label}
}> e.stopPropagation()} onKeyDown={(e) => { if (e.key === 'Enter') { e.stopPropagation(); setRenamingFile(null); props.onRenameFile(item.id, e.currentTarget.value); } else if (e.key === 'Escape') { setRenamingFile(null); } }} onBlur={(e) => { if (isRenaming()) { setRenamingFile(null); props.onRenameFile(item.id, e.currentTarget.value); } }} />
{ e.stopPropagation(); setActiveMenu(activeMenu() === item.id ? null : item.id); }} />
activeMenu() === item.id} setOpen={(val) => { if (!val) setActiveMenu(null); }} menuButton={() => btnRef} > setActiveMenu(null)}> { handleSelect(item); setActiveMenu(null); }} /> { setRenamingFile(item.id); setActiveMenu(null); }} /> { if (confirm(`Delete ${item.label}?`)) { props.onDeleteFile(item.id); } setActiveMenu(null); }} />
); }}
)}

No results found for "{query()}"

); }; ================================================ FILE: packages/solid-repl/src/components/preview.tsx ================================================ import { Component, createEffect, JSX, onCleanup, onMount } from 'solid-js'; import { useZoom } from '../hooks/useZoom'; import { Orientation, SplitviewComponent } from 'dockview-core'; import { SolidPanelView } from '../dockview/solid'; const dispatchZoomKeyToParent = ` document.addEventListener('keydown', (e) => { if (!(e.ctrlKey || e.metaKey)) return; if (!['=', '-'].includes(e.key)) return; window.parent.postMessage({ event: 'ZOOM_KEY', value: { key: e.key, ctrlKey: e.ctrlKey, metaKey: e.metaKey } }, '*'); e.preventDefault(); }, true); `; // Sandboxed iframes get a unique opaque origin, which causes two problems for chobitsu: // 1. localStorage / sessionStorage throw on access (opaque origins have no storage). // 2. chobitsu's getUrl()/getOrigin() fall back to parent.location.{href,origin} for // "about:" / "null"-origin pages, and that read is cross-origin and throws — which // blanks out Page.getResourceTree, so chii's Sources panel stays empty. // We install in-memory storage shims and replace `parent` with a thin object that returns // the iframe's own location while forwarding postMessage to the real parent (so we can // still talk to the playground). const sandboxShim = ` (() => { const make = () => { const m = new Map(); return { getItem: (k) => (m.has(k) ? m.get(k) : null), setItem: (k, v) => { m.set(k, String(v)); }, removeItem: (k) => { m.delete(k); }, clear: () => { m.clear(); }, key: (i) => Array.from(m.keys())[i] ?? null, get length() { return m.size; }, }; }; Object.defineProperty(window, 'localStorage', { value: make(), configurable: true }); Object.defineProperty(window, 'sessionStorage', { value: make(), configurable: true }); const realParent = window.parent; window.parent = { location: { href: location.href, origin: location.origin || 'about:srcdoc' }, postMessage: (msg, target, transfer) => realParent.postMessage(msg, target, transfer), }; })(); `; const mainIframeScript = ` (() => { let finisher = undefined; let cache = {}; const buildModule = (name, source, sources) => { if (cache[name]) return cache[name]; cache[name] = 'error:cyclic import'; const out = source.replace(/(['"])solidrepl:([^'"]+)\\1/g, (_, q, rel) => { if (sources[rel] == null) return q + rel + q; return q + buildModule(rel, sources[rel], sources) + q; }); const blob = new Blob([out], { type: 'text/javascript' }); cache[name] = URL.createObjectURL(blob); return cache[name]; }; const handleCodeUpdate = (sources) => { if (!sources || typeof sources['./main'] !== 'string') return; window.dispose?.(); window.dispose = undefined; if (document.getElementById('app')) document.getElementById('app').innerHTML = ''; console.clear(); document.getElementById('appsrc')?.remove(); for (const url of Object.values(cache)) { if (typeof url === 'string' && url.startsWith('blob:')) URL.revokeObjectURL(url); } cache = {}; const script = document.createElement('script'); script.id = 'appsrc'; script.type = 'module'; finisher = () => {}; script.onload = () => { if (finisher) finisher(); finisher = undefined; }; script.src = buildModule('./main', sources['./main'], sources); document.body.appendChild(script); const load = document.getElementById('load'); if (load) load.remove(); }; const sendToDevtools = (message) => { window.parent.postMessage(JSON.stringify(message), '*'); }; let id = 0; const sendToChobitsu = (message) => { message.id = 'tmp' + ++id; chobitsu.sendRawMessage(JSON.stringify(message)); }; chobitsu.setOnMessage((message) => { if (message.includes('"id":"tmp')) return; window.parent.postMessage(message, '*'); }); let pageSource = ''; const pageDomain = chobitsu.domain('Page'); if (pageDomain) { pageDomain.getResourceContent = (params) => { if (params.frameId === '1') { return Promise.resolve({ base64Encoded: false, content: pageSource }); } return Promise.resolve({ base64Encoded: false, content: '' }); }; } const handle = (data) => { try { const { event, value } = data; if (event === 'CODE_UPDATE') { const next = () => handleCodeUpdate(value); if (finisher !== undefined) finisher = next; else next(); } else if (event === 'IMPORT_MAP') { document.getElementById('importmap')?.remove(); const importMap = document.createElement('script'); importMap.id = 'importmap'; importMap.type = 'importmap'; importMap.textContent = JSON.stringify({ imports: value }); document.head.appendChild(importMap); } else if (event === 'DARK') { document.documentElement.classList.toggle('dark', value); } else if (event === 'PAGE_SOURCE') { pageSource = value; } else if (event === 'DEV') { chobitsu.sendRawMessage(data.data); } else if (event === 'LOADED') { sendToDevtools({ method: 'Page.frameNavigated', params: { frame: { id: '1', mimeType: 'text/html', securityOrigin: parent.location.origin, url: parent.location.href }, type: 'Navigation', }, }); sendToChobitsu({ method: 'Network.enable' }); sendToDevtools({ method: 'Runtime.executionContextsCleared' }); sendToChobitsu({ method: 'Runtime.enable' }); sendToChobitsu({ method: 'Debugger.enable' }); sendToChobitsu({ method: 'DOMStorage.enable' }); sendToChobitsu({ method: 'DOM.enable' }); sendToChobitsu({ method: 'CSS.enable' }); sendToChobitsu({ method: 'Overlay.enable' }); sendToDevtools({ method: 'DOM.documentUpdated' }); } } catch (e) { console.error(e); } }; window.addEventListener('message', (e) => handle(e.data)); ${dispatchZoomKeyToParent} })(); `; const iframeHtml = `

Loading the playground...

`; const useDevtoolsSrc = () => { const html = ` DevTools `; const devtoolsRawUrl = URL.createObjectURL(new Blob([html], { type: 'text/html' })); onCleanup(() => URL.revokeObjectURL(devtoolsRawUrl)); return `${devtoolsRawUrl}#?embedded=${encodeURIComponent(location.origin)}`; }; export const Preview: Component = (props) => { const { zoomState } = useZoom(); let iframe!: HTMLIFrameElement; let devtoolsIframe!: HTMLIFrameElement; let outerContainer!: HTMLDivElement; let devtoolsLoaded = false; let isIframeReady = false; const sendToIframe = (msg: any) => { if (!isIframeReady) return; iframe.contentWindow!.postMessage(msg, '*'); }; createEffect(() => { if (!props.reloadSignal) return; isIframeReady = false; iframe.srcdoc = iframeHtml; }); const devtoolsSrc = useDevtoolsSrc(); const styleScale = () => { const pointerEvents = props.pointerEvents ? 'inherit' : 'none'; if (zoomState.scale === 100 || !zoomState.scaleIframe) return `pointer-events: ${pointerEvents};`; return `pointer-events: ${pointerEvents}; width: ${zoomState.scale}%; height: ${zoomState.scale}%; transform: scale(${ zoomState.zoom / 100 }); transform-origin: 0 0;`; }; onMount(() => { const frameworkComponents: Record JSX.Element> = { preview: () => (