├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── public ├── index.html ├── manifest.json └── robots.txt ├── src ├── app.module.css ├── app.tsx ├── components │ ├── editor.tsx │ ├── icons.tsx │ ├── log-item.tsx │ ├── log-items │ │ ├── array.module.css │ │ ├── array.tsx │ │ ├── base.module.css │ │ ├── base.tsx │ │ ├── error.module.css │ │ ├── error.tsx │ │ ├── function.module.css │ │ ├── function.tsx │ │ ├── html.module.css │ │ ├── html.tsx │ │ ├── map.module.css │ │ ├── map.tsx │ │ ├── object.module.css │ │ ├── object.tsx │ │ ├── promise.module.css │ │ ├── promise.tsx │ │ ├── proxy.module.css │ │ ├── proxy.tsx │ │ ├── set.module.css │ │ ├── set.tsx │ │ ├── string.module.css │ │ └── string.tsx │ ├── logs.module.css │ ├── logs.tsx │ ├── runner.tsx │ ├── toolbar.module.css │ └── toolbar.tsx ├── context │ └── console.tsx ├── index.css ├── index.tsx ├── libs │ ├── constants.ts │ ├── editor.ts │ ├── log.ts │ └── utils.ts └── setupTests.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sonny T. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript/TypeScript Console 2 | 3 | Simple JavaScript/TypeScript console. Open source alternative to [JSFiddle](https://jsfiddle.net) and [JS Bin](https://jsbin.com). 4 | 5 | ![418shots_so](https://github.com/sonnyt/console/assets/183387/bcb1ce20-c983-43c9-aefa-5ce8d546eb82) 6 | 7 | ## Try it out 8 | Try out the console [here](https://console.sonnyt.com). 9 | 10 | ## Why? 11 | I needed a convenient way to quickly run JavaScript and TypeScript code without having to open VSCode or a terminal. Other alternatives I found were either too bloated with a heavy focus on HTML and CSS, or too basic with unreliable logging. So, I decided to build my own code runner using the same editor that VSCode uses, which provides a lot of handy features right out of the box. 12 | 13 | ## Features 14 | - TypeScript support 15 | - Code completion 16 | - Decent logging capabilities 17 | - Lightweight and user-friendly 18 | - Localstorage support for data persistence 19 | - Powered by [Monaco](https://microsoft.github.io/monaco-editor/) editor for a rich editing experience 20 | 21 | ## Shortcuts 22 | - `CMD/CTRL+ENTER` - Runs the code. 23 | - `CMD/CTRL+K` - Clears the logs. 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-console", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@monaco-editor/react": "^4.6.0", 7 | "@testing-library/jest-dom": "^5.17.0", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.18.71", 12 | "@types/react": "^18.2.48", 13 | "@types/react-dom": "^18.2.18", 14 | "lz-string": "^1.5.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-resizable-panels": "^1.0.9", 18 | "react-scripts": "5.0.1", 19 | "typescript": "^4.9.5", 20 | "web-vitals": "^2.1.4" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@types/css-modules": "^1.0.5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | JS/TS Console 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "JS Console", 3 | "name": "Simple JavaScript playground", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "theme_color": "#1e2227", 7 | "background_color": "#1e2227" 8 | } 9 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/app.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | background-color: var(--bg-color); 4 | height: calc(100vh - var(--toolbar-height)); 5 | } 6 | 7 | .resize { 8 | width: 5px; 9 | opacity: .5; 10 | margin-left: -1px; 11 | position: relative; 12 | background-color: var(--border-color); 13 | } 14 | 15 | .resize:hover { 16 | opacity: 1; 17 | } -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Panel, PanelResizeHandle, PanelGroup } from "react-resizable-panels"; 3 | import { compress, decompress } from "lz-string"; 4 | 5 | import { ConsoleProvider } from "./context/console"; 6 | import Editor, { type EditorProps } from "./components/editor"; 7 | import Logs from "./components/logs"; 8 | import Toolbar from "./components/toolbar"; 9 | import Runner from "./components/runner"; 10 | import styles from "./app.module.css"; 11 | 12 | export type ConsoleProps = EditorProps & {}; 13 | 14 | export const Console = (props: ConsoleProps) => { 15 | return ( 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |
31 | ); 32 | }; 33 | 34 | export default function App() { 35 | const [code, setCode] = useState(""); 36 | 37 | const handleOnChange = (code?: string) => { 38 | setCode(code || ""); 39 | const compressed = compress(code || ""); 40 | localStorage.setItem("console:code", compressed); 41 | }; 42 | 43 | useEffect(() => { 44 | const compressed = localStorage.getItem("console:code"); 45 | if (compressed) { 46 | const code = decompress(compressed); 47 | setCode(code || ""); 48 | } 49 | }, []); 50 | 51 | return ; 52 | } 53 | -------------------------------------------------------------------------------- /src/components/editor.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { 3 | Editor as CodeEditor, 4 | useMonaco, 5 | type OnMount, 6 | type OnChange, 7 | } from "@monaco-editor/react"; 8 | 9 | import { useConsole } from "../context/console"; 10 | import { darkTheme, options } from "../libs/editor"; 11 | 12 | export type EditorProps = { 13 | defaultValue?: string; 14 | onChange: (code?: string) => void; 15 | }; 16 | 17 | export default function Editor({ defaultValue, onChange }: EditorProps) { 18 | const monaco = useMonaco(); 19 | const [state, dispatch] = useConsole(); 20 | 21 | useEffect(() => { 22 | if (!monaco) { 23 | return; 24 | } 25 | 26 | monaco.editor.defineTheme("onedark-pro", darkTheme as any); 27 | monaco.editor.setTheme("onedark-pro"); 28 | 29 | monaco.editor.addEditorAction({ 30 | id: "execute_code", 31 | label: "Run Code", 32 | keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], 33 | contextMenuGroupId: "navigation", 34 | contextMenuOrder: 0, 35 | run: () => dispatch({ type: "RUN_CODE" }), 36 | }); 37 | 38 | monaco.editor.addEditorAction({ 39 | id: "clear_console", 40 | label: "Clear Console", 41 | keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK], 42 | contextMenuGroupId: "navigation", 43 | contextMenuOrder: 1, 44 | run: () => dispatch({ type: "CLEAR_LOGS" }), 45 | }); 46 | }, [monaco, dispatch]); 47 | 48 | const handleEditorDidMount: OnMount = (editor) => { 49 | editor.focus(); 50 | dispatch({ type: "SET_EDITOR", payload: { editor } }); 51 | }; 52 | 53 | const handleOnChange: OnChange = (code?: string) => { 54 | dispatch({ type: "SET_CODE", payload: { code } }); 55 | onChange(code); 56 | }; 57 | 58 | return ( 59 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/components/icons.tsx: -------------------------------------------------------------------------------- 1 | const Icons = { 2 | loading: ( 3 | <> 4 | 5 | 6 | 7 | ), 8 | run: ( 9 | <> 10 | 11 | 16 | 17 | ), 18 | error: ( 19 | <> 20 | 21 | 22 | 23 | 24 | 25 | ), 26 | warning: ( 27 | <> 28 | 29 | 30 | 31 | 32 | 33 | ), 34 | right: ( 35 | <> 36 | 37 | 38 | 39 | ), 40 | down: ( 41 | <> 42 | 43 | 44 | 45 | ), 46 | clear: ( 47 | <> 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | ), 56 | github: ( 57 | <> 58 | 59 | 60 | 61 | ) 62 | }; 63 | 64 | function IconComponent(type: keyof typeof Icons) { 65 | return function Icon({ ...props }: React.SVGAttributes) { 66 | return ( 67 | 77 | {Icons[type]} 78 | 79 | ); 80 | }; 81 | } 82 | 83 | // eslint-disable-next-line import/no-anonymous-default-export 84 | export default { 85 | Loading: IconComponent("loading"), 86 | Run: IconComponent("run"), 87 | Error: IconComponent("error"), 88 | Warning: IconComponent("warning"), 89 | Right: IconComponent("right"), 90 | Down: IconComponent("down"), 91 | Clear: IconComponent("clear"), 92 | Github: IconComponent("github"), 93 | }; 94 | -------------------------------------------------------------------------------- /src/components/log-item.tsx: -------------------------------------------------------------------------------- 1 | import { ValueTypes } from "../libs/constants"; 2 | import ProxyLog from "./log-items/proxy"; 3 | import StringLog from "./log-items/string"; 4 | import PromiseLog from "./log-items/promise"; 5 | import FunctionLog from "./log-items/function"; 6 | import ErrorLog from "./log-items/error"; 7 | import ArrayLog from "./log-items/array"; 8 | import MapLog from "./log-items/map"; 9 | import SetLog from "./log-items/set"; 10 | import HTMLLog from "./log-items/html"; 11 | import ObjectLog from "./log-items/object"; 12 | import { getType } from "../libs/utils"; 13 | 14 | type Props = { 15 | logs: any[]; 16 | scope: WeakMap; 17 | isMinimized: boolean; 18 | }; 19 | 20 | export default function LogItem({ logs, scope, isMinimized = true }: Props) { 21 | return ( 22 | <> 23 | {logs.map((log, index) => { 24 | const type = scope.get(log)?.isProxy ? ValueTypes.PROXY : getType(log); 25 | 26 | if (type === ValueTypes.PROXY) { 27 | return ( 28 | 34 | ); 35 | } 36 | 37 | if (type === ValueTypes.NULL) { 38 | return ; 39 | } 40 | 41 | if (type === ValueTypes.UNDEFINED) { 42 | return ; 43 | } 44 | 45 | if (type === ValueTypes.NUMBER) { 46 | return ; 47 | } 48 | 49 | if (type === ValueTypes.STRING) { 50 | return ; 51 | } 52 | 53 | if (type === ValueTypes.BOOLEAN) { 54 | return ; 55 | } 56 | 57 | if (type === ValueTypes.DATE) { 58 | return ; 59 | } 60 | 61 | if (type === ValueTypes.REGEXP) { 62 | return ; 63 | } 64 | 65 | if (type === ValueTypes.SYMBOL) { 66 | return ; 67 | } 68 | 69 | if (type === ValueTypes.PROMISE) { 70 | return ( 71 | 77 | ); 78 | } 79 | 80 | if (type === ValueTypes.FUNCTION) { 81 | return ( 82 | 87 | ); 88 | } 89 | 90 | if (type === ValueTypes.ERROR) { 91 | return ( 92 | 97 | ); 98 | } 99 | 100 | if (type === ValueTypes.ARRAY) { 101 | return ( 102 | 108 | ); 109 | } 110 | 111 | if (type === ValueTypes.MAP) { 112 | return ( 113 | 119 | ); 120 | } 121 | 122 | if (type === ValueTypes.SET) { 123 | return ( 124 | 130 | ); 131 | } 132 | 133 | if (type === ValueTypes.HTML_ELEMENT) { 134 | return ( 135 | 141 | ); 142 | } 143 | 144 | if (type === ValueTypes.OBJECT) { 145 | return ( 146 | 152 | ); 153 | } 154 | 155 | return ( 156 | 161 | ); 162 | })} 163 | 164 | ); 165 | } 166 | -------------------------------------------------------------------------------- /src/components/log-items/array.module.css: -------------------------------------------------------------------------------- 1 | .is_array { 2 | font-style: italic; 3 | } -------------------------------------------------------------------------------- /src/components/log-items/array.tsx: -------------------------------------------------------------------------------- 1 | import { Collapsable, PropertyList } from "./base"; 2 | import StringLog from "./string"; 3 | import LogItem from "../log-item"; 4 | import styles from "./array.module.css"; 5 | 6 | type Props = { 7 | log: unknown[]; 8 | scope: WeakMap; 9 | isMinimized: boolean; 10 | }; 11 | 12 | export default function ArrayLog({ log, scope, isMinimized }: Props) { 13 | if (isMinimized) { 14 | return ; 15 | } 16 | 17 | return ( 18 | 19 | <> 20 | ({log.length}) [ 21 | {log.slice(0, 5).map((item, index) => { 22 | return ( 23 | <> 24 | 25 | {index < log.length - 1 && ","} 26 | 27 | ); 28 | })} 29 | {log.length > 5 && " …"}] 30 | 31 | <> 32 | [index, value])} 35 | size={{ key: "length", value: log.length }} 36 | /> 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/components/log-items/base.module.css: -------------------------------------------------------------------------------- 1 | .log { 2 | font-size: .75rem; 3 | line-height: 1.5; 4 | display: inline-block; 5 | white-space: pre; 6 | } 7 | 8 | .log + .log { 9 | margin-left: 0.5rem; 10 | vertical-align: top; 11 | } 12 | 13 | .collapse { 14 | cursor: pointer; 15 | margin-left: -4px; 16 | } 17 | 18 | .collapse:hover { 19 | background-color: var(--highlight-color); 20 | } 21 | 22 | .icon { 23 | margin-left: -3px; 24 | position: relative; 25 | top: 2px; 26 | } 27 | 28 | .properties { 29 | list-style: none; 30 | padding: 0; 31 | margin: 0; 32 | font-style: normal; 33 | padding-left: 1rem; 34 | margin-left: 0.25rem; 35 | border-left: 1px solid var(--border-color); 36 | } 37 | 38 | .properties > li > div { 39 | margin-left: 0; 40 | } 41 | 42 | .length { 43 | opacity: .5; 44 | } 45 | 46 | .key { 47 | vertical-align: top; 48 | } 49 | 50 | .no_props { 51 | font-style: italic; 52 | color: var(--color-muted); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/log-items/base.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Icons from "../icons"; 3 | import styles from "./base.module.css"; 4 | import LogItem from "../log-item"; 5 | import StringLog from "./string"; 6 | 7 | type LogWrapperProps = React.PropsWithChildren< 8 | React.HTMLAttributes 9 | >; 10 | 11 | export function LogWrapper({ children, className, ...props }: LogWrapperProps) { 12 | return ( 13 |
14 | {children} 15 |
16 | ); 17 | } 18 | 19 | type CollapsableProps = React.HTMLAttributes & { 20 | toggle?: boolean; 21 | }; 22 | 23 | export function Collapsable({ 24 | toggle = false, 25 | children, 26 | ...props 27 | }: React.PropsWithChildren) { 28 | const [isCollapsed, setIsCollapsed] = useState(true); 29 | 30 | const onToggle = (e: React.MouseEvent) => { 31 | e.stopPropagation(); 32 | setIsCollapsed(!isCollapsed); 33 | }; 34 | 35 | const components = React.Children.toArray(children); 36 | 37 | return ( 38 | 43 | {isCollapsed ? ( 44 | 45 | ) : ( 46 | 47 | )} 48 | {toggle ? ( 49 | isCollapsed ? ( 50 | components[0] 51 | ) : ( 52 | components[1] 53 | ) 54 | ) : ( 55 | <> 56 | {components[0]} 57 | {!isCollapsed && components[1]} 58 | 59 | )} 60 | 61 | ); 62 | } 63 | 64 | type PropertyListProps = { 65 | scope: WeakMap; 66 | list: [string | number, any][]; 67 | size?: { 68 | key: string; 69 | value: number; 70 | }; 71 | }; 72 | 73 | export function PropertyList({ scope, list, size }: PropertyListProps) { 74 | return ( 75 |
    76 | {list.map(([key, value], index) => { 77 | return ( 78 |
  • 79 | {key}:{" "} 80 | 81 |
  • 82 | ); 83 | })} 84 | 85 | {size && ( 86 |
  • 87 | {size.key}:{" "} 88 | 89 |
  • 90 | )} 91 | 92 | {list.length === 0 && !size && ( 93 |
  • 94 | No properties 95 |
  • 96 | )} 97 |
98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /src/components/log-items/error.module.css: -------------------------------------------------------------------------------- 1 | .is_error { 2 | display: inline; 3 | color: var(--color-ansi-bright-red); 4 | } -------------------------------------------------------------------------------- /src/components/log-items/error.tsx: -------------------------------------------------------------------------------- 1 | import { Collapsable } from "./base"; 2 | import StringLog from "./string"; 3 | import styles from "./error.module.css"; 4 | 5 | type Props = { 6 | log: Error; 7 | isMinimized: boolean; 8 | }; 9 | 10 | export default function ErrorLog({ log, isMinimized }: Props) { 11 | if (isMinimized) { 12 | return ; 13 | } 14 | 15 | return ( 16 | 17 | <>{log.message} 18 | <> 19 |
20 | {log.stack} 21 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/log-items/function.module.css: -------------------------------------------------------------------------------- 1 | .symbol { 2 | color: var(--color-ansi-yellow); 3 | } -------------------------------------------------------------------------------- /src/components/log-items/function.tsx: -------------------------------------------------------------------------------- 1 | import { LogWrapper } from "./base"; 2 | import { parseFunction } from "../../libs/utils"; 3 | import styles from "./function.module.css"; 4 | 5 | type Props = { 6 | log: Function; 7 | isMinimized: boolean; 8 | }; 9 | 10 | export default function FunctionLog({ log, isMinimized }: Props) { 11 | const func = parseFunction(log); 12 | 13 | return ( 14 | 15 | {isMinimized ? ( 16 | func.symbol 17 | ) : ( 18 | <> 19 | {func.symbol} {func.body} 20 | 21 | )} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/log-items/html.module.css: -------------------------------------------------------------------------------- 1 | .is_html { 2 | margin-left: .75rem; 3 | } 4 | 5 | .is_comment { 6 | color: var(--color-muted); 7 | } 8 | 9 | .is_text { 10 | color: var(--text-color); 11 | } 12 | 13 | .is_element { 14 | color: var(--color-ansi-blue); 15 | } 16 | 17 | .is_element > .is_attribute { 18 | margin-left: .35rem; 19 | } 20 | 21 | .is_attribute { 22 | color: var(--text-color); 23 | } 24 | 25 | .is_attribute > span:first-child { 26 | color: var(--color-ansi-bright-blue); 27 | } 28 | 29 | .is_attribute > span:last-child { 30 | color: var(--color-ansi-yellow); 31 | } 32 | 33 | .close_tag { 34 | margin-left: -0.75rem; 35 | } 36 | 37 | .is_minimized > span:nth-child(1) { 38 | color: var(--color-ansi-blue); 39 | } 40 | 41 | .is_minimized > span:nth-child(2) { 42 | color: var(--color-ansi-yellow); 43 | } 44 | 45 | .is_minimized > span:nth-child(3) { 46 | color: var(--color-ansi-bright-blue); 47 | } 48 | 49 | .dom_tree { 50 | list-style: none; 51 | padding: 0; 52 | margin: 0; 53 | font-style: normal; 54 | padding-left: 1rem; 55 | margin-left: 0.25rem; 56 | border-left: 1px solid var(--border-color); 57 | } 58 | 59 | .dom_tree > li > .is_html { 60 | margin-left: 0; 61 | } -------------------------------------------------------------------------------- /src/components/log-items/html.tsx: -------------------------------------------------------------------------------- 1 | import { Collapsable, LogWrapper } from "./base"; 2 | import LogItem from "../log-item"; 3 | import StringLog from "./string"; 4 | import { NodeTypes } from "../../libs/constants"; 5 | import styles from "./html.module.css"; 6 | 7 | type HTMLTagProps = { 8 | elm: Element; 9 | scope: WeakMap; 10 | }; 11 | 12 | const HTMLTag = ({ elm, children }: React.PropsWithChildren) => { 13 | return ( 14 | <> 15 | {"<"} 16 | {elm.tagName.toLowerCase()} 17 | {Array.from(elm.attributes).map((attr, index) => ( 18 | 19 | ))} 20 | {">"} 21 | {children} 22 | {``} 23 | 24 | ); 25 | }; 26 | 27 | const MinimizedString = ({ 28 | children, 29 | ...props 30 | }: React.PropsWithChildren>) => { 31 | return ( 32 | 36 | {children} 37 | 38 | ); 39 | }; 40 | 41 | type DomTreeProps = { 42 | nodes: NodeListOf; 43 | scope: WeakMap; 44 | }; 45 | 46 | const DomTree = ({ 47 | nodes, 48 | scope, 49 | children, 50 | }: React.PropsWithChildren) => { 51 | return ( 52 |
    53 | {Array.from(nodes).map((child, index) => ( 54 |
  • 55 | 56 |
  • 57 | ))} 58 | {children} 59 |
60 | ); 61 | }; 62 | 63 | type DocumentNodeProps = { 64 | node: Document; 65 | scope: WeakMap; 66 | isMinimized: boolean; 67 | }; 68 | 69 | const DocumentNode = ({ node, scope, isMinimized }: DocumentNodeProps) => { 70 | const href = node.location?.href ?? "about:blank"; 71 | 72 | if (isMinimized) { 73 | return ( 74 | 75 | document 76 | 77 | ); 78 | } 79 | 80 | return ( 81 | 82 | <> 83 | 84 | 85 | <> 86 | 87 | 88 | 89 | ); 90 | }; 91 | 92 | type DocumentFragmentNodeProps = { 93 | node: DocumentFragment; 94 | scope: WeakMap; 95 | isMinimized: boolean; 96 | }; 97 | 98 | const DocumentFragmentNode = ({ 99 | node, 100 | scope, 101 | isMinimized, 102 | }: DocumentFragmentNodeProps) => { 103 | if (isMinimized) { 104 | return ( 105 | 106 | document-fragment 107 | 108 | ); 109 | } 110 | 111 | return ( 112 | 113 | <> 114 | 115 | 116 | <> 117 | 118 | 119 | 120 | ); 121 | }; 122 | 123 | type TextNodeProps = { 124 | node: Text; 125 | isMinimized: boolean; 126 | }; 127 | 128 | const TextNode = ({ node, isMinimized }: TextNodeProps) => { 129 | if (isMinimized) { 130 | return ( 131 | 132 | text 133 | 134 | ); 135 | } 136 | 137 | return ( 138 | 142 | ); 143 | }; 144 | 145 | type AttrNodeProps = { 146 | node: Attr; 147 | isMinimized: boolean; 148 | }; 149 | 150 | const AttrNode = ({ node, isMinimized }: AttrNodeProps) => { 151 | if (isMinimized) { 152 | return ( 153 | 154 | 155 | 156 | {node.name} 157 | 158 | ); 159 | } 160 | 161 | return ( 162 | 163 | {node.name}="{node.value}" 164 | 165 | ); 166 | }; 167 | 168 | type CommentNodeProps = { 169 | node: Comment; 170 | isMinimized: boolean; 171 | }; 172 | 173 | const CommentNode = ({ node, isMinimized }: CommentNodeProps) => { 174 | if (isMinimized) { 175 | return ( 176 | 177 | comment 178 | 179 | ); 180 | } 181 | 182 | return ( 183 | `} 186 | /> 187 | ); 188 | }; 189 | 190 | type ProcessingInstructionNodeProps = { 191 | node: ProcessingInstruction; 192 | isMinimized: boolean; 193 | }; 194 | 195 | const ProcessingInstructionNode = ({ 196 | node, 197 | isMinimized, 198 | }: ProcessingInstructionNodeProps) => { 199 | if (isMinimized) { 200 | return ( 201 | 202 | {node.target} 203 | 204 | ); 205 | } 206 | 207 | return ; 208 | }; 209 | 210 | type ElementNodeProps = { 211 | node: Element; 212 | scope: WeakMap; 213 | isMinimized: boolean; 214 | }; 215 | 216 | const ElementNode = ({ node, scope, isMinimized }: ElementNodeProps) => { 217 | if (isMinimized) { 218 | const tagName = node.tagName.toLowerCase(); 219 | const classNames = node.classList.value.replaceAll(" ", "."); 220 | 221 | return ( 222 | 223 | <> 224 | {tagName} 225 | {node.id && `#${node.id}`} 226 | {node.classList.length > 0 && `.${classNames}`} 227 | 228 | 229 | ); 230 | } 231 | 232 | if (node.childNodes.length === 0) { 233 | return ( 234 | 235 | 236 | 237 | ); 238 | } 239 | 240 | return ( 241 | 245 | <> 246 | 247 | {"{…}"} 248 | 249 | 250 | <> 251 | {"<"} 252 | {node.tagName.toLowerCase()} 253 | {Array.from(node.attributes).map((attr, index) => ( 254 | 259 | ))} 260 | {">"} 261 | 262 |
  • 263 | {``} 264 |
  • 265 |
    266 | 267 |
    268 | ); 269 | }; 270 | 271 | type HTMLLogProps = { 272 | log: 273 | | Element 274 | | Attr 275 | | Text 276 | | Comment 277 | | ProcessingInstruction 278 | | Document 279 | | DocumentFragment; 280 | scope: WeakMap; 281 | isMinimized: boolean; 282 | }; 283 | 284 | export default function HTMLLog({ log, scope, isMinimized }: HTMLLogProps) { 285 | if (log.nodeType === NodeTypes.DOCUMENT_NODE) { 286 | return ( 287 | 292 | ); 293 | } 294 | 295 | const node = log.cloneNode(true); 296 | 297 | if (log.nodeType === NodeTypes.DOCUMENT_FRAGMENT_NODE) { 298 | return ( 299 | 304 | ); 305 | } 306 | 307 | if (node.nodeType === NodeTypes.TEXT_NODE) { 308 | return ; 309 | } 310 | 311 | if (node.nodeType === NodeTypes.ATTRIBUTE_NODE) { 312 | return ; 313 | } 314 | 315 | if (node.nodeType === NodeTypes.COMMENT_NODE) { 316 | return ; 317 | } 318 | 319 | if (node.nodeType === NodeTypes.PROCESSING_INSTRUCTION_NODE) { 320 | return ( 321 | 325 | ); 326 | } 327 | 328 | if (node.nodeType === NodeTypes.ELEMENT_NODE) { 329 | return ( 330 | 335 | ); 336 | } 337 | 338 | return null; 339 | } 340 | -------------------------------------------------------------------------------- /src/components/log-items/map.module.css: -------------------------------------------------------------------------------- 1 | .is_map { 2 | font-style: italic; 3 | } 4 | 5 | .key { 6 | color: var(--color-muted); 7 | } -------------------------------------------------------------------------------- /src/components/log-items/map.tsx: -------------------------------------------------------------------------------- 1 | import { Collapsable, PropertyList } from "./base"; 2 | import styles from "./map.module.css"; 3 | import StringLog from "./string"; 4 | import LogItem from "../log-item"; 5 | 6 | type Props = { 7 | log: Map; 8 | scope: WeakMap; 9 | isMinimized: boolean; 10 | }; 11 | 12 | export default function MapLog({ log, scope, isMinimized }: Props) { 13 | const mapArray = Array.from(log); 14 | 15 | if (isMinimized) { 16 | return ; 17 | } 18 | 19 | return ( 20 | 21 | <> 22 | Map({`${log.size}`}) {"{"} 23 | {mapArray.slice(0, 5).map(([key, item], index) => { 24 | return ( 25 | <> 26 | {key} 27 | {" =>"} 28 | {index < mapArray.length - 1 && ", "} 29 | 30 | ); 31 | })} 32 | {mapArray.length > 5 && "…"} 33 | {"}"} 34 | 35 | <> 36 | 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/components/log-items/object.module.css: -------------------------------------------------------------------------------- 1 | .is_object { 2 | font-style: italic; 3 | } 4 | 5 | .key { 6 | color: var(--color-muted); 7 | } -------------------------------------------------------------------------------- /src/components/log-items/object.tsx: -------------------------------------------------------------------------------- 1 | import { Collapsable, PropertyList } from "./base"; 2 | import styles from "./object.module.css"; 3 | import StringLog from "./string"; 4 | import LogItem from "../log-item"; 5 | import { getObjectName } from "../../libs/utils"; 6 | 7 | type Props = { 8 | log: Record; 9 | scope: WeakMap; 10 | isMinimized: boolean; 11 | }; 12 | 13 | export default function ObjectLog({ log, scope, isMinimized }: Props) { 14 | let prefix: string | null = getObjectName(log); 15 | prefix = prefix === "Object" ? null : prefix; 16 | 17 | const objKeys = Object.getOwnPropertyNames(log); 18 | 19 | if (isMinimized) { 20 | return ; 21 | } 22 | 23 | return ( 24 | 25 | <> 26 | {prefix ? `${prefix} {` : "{"} 27 | {objKeys.slice(0, 5).map((key, index) => { 28 | return ( 29 | <> 30 | {key}:{" "} 31 | 32 | {index < objKeys.length - 1 && ", "} 33 | 34 | ); 35 | })} 36 | {objKeys.length > 5 && "…"} 37 | {"}"} 38 | 39 | <> 40 | [key, log[key]])} 43 | /> 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/log-items/promise.module.css: -------------------------------------------------------------------------------- 1 | .is_promise { 2 | font-style: italic; 3 | } 4 | 5 | .state { 6 | color: var(--color-muted); 7 | } -------------------------------------------------------------------------------- /src/components/log-items/promise.tsx: -------------------------------------------------------------------------------- 1 | import { Collapsable, PropertyList } from "./base"; 2 | import StringLog from "./string"; 3 | import LogItem from "../log-item"; 4 | import styles from "./promise.module.css"; 5 | 6 | type Props = { 7 | log: Promise; 8 | scope: WeakMap; 9 | isMinimized: boolean; 10 | }; 11 | 12 | export default function PromiseLog({ log, scope, isMinimized }: Props) { 13 | if (isMinimized) { 14 | return ; 15 | } 16 | 17 | const meta = scope.get(log); 18 | 19 | return ( 20 | 21 | <> 22 | Promise {"{"} 23 | 24 | {"<"} 25 | {meta.state} 26 | {">"} 27 | 28 | {meta.value && ( 29 | <> 30 | : 31 | 32 | )} 33 | {"}"} 34 | 35 | <> 36 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/log-items/proxy.module.css: -------------------------------------------------------------------------------- 1 | .is_proxy { 2 | font-style: italic; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/log-items/proxy.tsx: -------------------------------------------------------------------------------- 1 | import { Collapsable, PropertyList } from "./base"; 2 | import LogItem from "../log-item"; 3 | import StringLog from "./string"; 4 | import styles from "./proxy.module.css"; 5 | 6 | type Props = { 7 | log: Record; 8 | scope: WeakMap; 9 | isMinimized: boolean; 10 | }; 11 | 12 | export default function ProxyLog({ log, scope, isMinimized }: Props) { 13 | if (isMinimized) { 14 | return ; 15 | } 16 | 17 | const meta = scope.get(log); 18 | 19 | return ( 20 | 21 | <> 22 | Proxy {"("} 23 | 24 | {")"} 25 | 26 | <> 27 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/log-items/set.module.css: -------------------------------------------------------------------------------- 1 | .is_set { 2 | font-style: italic; 3 | } -------------------------------------------------------------------------------- /src/components/log-items/set.tsx: -------------------------------------------------------------------------------- 1 | import { Collapsable, PropertyList } from "./base"; 2 | import styles from "./set.module.css"; 3 | import StringLog from "./string"; 4 | import LogItem from "../log-item"; 5 | 6 | type Props = { 7 | log: Set; 8 | scope: WeakMap; 9 | isMinimized: boolean; 10 | }; 11 | 12 | export default function SetLog({ log, scope, isMinimized }: Props) { 13 | const setArray = Array.from(log); 14 | 15 | if (isMinimized) { 16 | return ; 17 | } 18 | 19 | return ( 20 | 21 | <> 22 | Set({`${log.size}`}) {"{"} 23 | {setArray.slice(0, 5).map((item, index) => { 24 | return ( 25 | <> 26 | 27 | {index < setArray.length - 1 && ","} 28 | 29 | ); 30 | })} 31 | {setArray.length > 5 && " …"} 32 | {"}"} 33 | 34 | <> 35 | [index, value])} 38 | size={{ key: "size", value: log.size }} 39 | /> 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/log-items/string.module.css: -------------------------------------------------------------------------------- 1 | .null, 2 | .undefined { 3 | color: var(--color-muted); 4 | } 5 | 6 | .string { 7 | color: var(--color-ansi-green); 8 | } 9 | 10 | .symbol { 11 | color: var(--color-ansi-cyan); 12 | } 13 | 14 | .boolean, 15 | .number { 16 | color: var(--color-ansi-blue); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/log-items/string.tsx: -------------------------------------------------------------------------------- 1 | import { LogWrapper } from "./base"; 2 | import { getType } from "../../libs/utils"; 3 | import styles from "./string.module.css"; 4 | 5 | type Props = React.HTMLAttributes & { 6 | log: any; 7 | }; 8 | 9 | export default function StringLog({ log, ...props }: Props) { 10 | const type = getType(log); 11 | 12 | return ( 13 | 14 | {log.toString()} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/logs.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100%; 3 | overflow: auto; 4 | } 5 | 6 | .logs { 7 | margin: 0; 8 | list-style: none; 9 | padding: 0.5rem 1rem; 10 | font-family: "Fira Code", "Fira Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace; 11 | } 12 | 13 | .logs > li { 14 | padding: 0.25rem; 15 | border-bottom: 1px solid var(--highlight-color); 16 | opacity: 0.75; 17 | display: block; 18 | line-height: 1; 19 | animation-iteration-count: initial; 20 | animation-name: highlight; 21 | animation-duration: 1s; 22 | } 23 | 24 | .logs > li.warn { 25 | background-color: rgb(240 164 93 / 5%); 26 | } 27 | 28 | .logs > li.warn .icon { 29 | color: var(--color-ansi-bright-yellow); 30 | } 31 | 32 | .logs > li.error { 33 | background-color: rgba(255 97 110 / 5%); 34 | } 35 | 36 | .logs > li.error .icon { 37 | color: var(--color-ansi-bright-red); 38 | } 39 | 40 | .icon { 41 | vertical-align: top; 42 | margin-right: 0.5rem; 43 | position: relative; 44 | top: 2px; 45 | } 46 | 47 | @keyframes highlight { 48 | from { 49 | background-color: #2c313c; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/logs.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from "react"; 2 | 3 | import { useConsole } from "../context/console"; 4 | import Icons from "./icons"; 5 | import styles from "./logs.module.css"; 6 | import LogItem from "./log-item"; 7 | 8 | export default function Logs() { 9 | const [state, dispatch] = useConsole(); 10 | const containerRef = useRef(null); 11 | 12 | useEffect(() => { 13 | if (!containerRef.current) { 14 | return; 15 | } 16 | 17 | containerRef.current.scrollTop = containerRef.current.scrollHeight; 18 | }, [containerRef, state.logs]); 19 | 20 | useEffect(() => { 21 | function handleKeyDown(e: KeyboardEvent) { 22 | if (e.metaKey && e.key === "k") { 23 | e.preventDefault(); 24 | e.stopPropagation(); 25 | dispatch({ type: "CLEAR_LOGS" }); 26 | } 27 | } 28 | 29 | window.addEventListener("keydown", handleKeyDown); 30 | 31 | return () => window.removeEventListener("keydown", handleKeyDown); 32 | }, [dispatch]); 33 | 34 | return ( 35 |
    36 |
      37 | {state.logs.map((log, index) => ( 38 |
    • 39 | {log.type === "error" && ( 40 | 41 | )} 42 | {log.type === "warn" && ( 43 | 44 | )} 45 | 46 |
    • 47 | ))} 48 |
    49 |
    50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/components/runner.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect, useMemo, useCallback } from "react"; 2 | import { useMonaco } from "@monaco-editor/react"; 3 | 4 | import { useConsole } from "../context/console"; 5 | import consoleStub, { promiseStub, proxyStub } from "../libs/log"; 6 | import { Languages } from "../libs/constants"; 7 | 8 | export default function Runner() { 9 | const monaco = useMonaco(); 10 | const [state, dispatch] = useConsole(); 11 | const iframeRef = useRef(null); 12 | 13 | const typescriptWorker = useMemo(async () => { 14 | const editor = state.editor; 15 | 16 | if (!editor || !monaco) { 17 | return; 18 | } 19 | 20 | const worker = await monaco.languages.typescript.getTypeScriptWorker(); 21 | return worker(editor.getModel()?.uri!); 22 | }, [state.editor, monaco]); 23 | 24 | const compileCode = useCallback(async () => { 25 | const service = await typescriptWorker; 26 | 27 | if (!service || !state.editor) { 28 | return null; 29 | } 30 | 31 | const { outputFiles } = await service.getEmitOutput( 32 | state.editor.getModel()?.uri.toString()! 33 | ); 34 | return outputFiles[0].text; 35 | }, [state.editor, typescriptWorker]); 36 | 37 | const runCode = useCallback( 38 | async (code: string | null) => { 39 | if (!iframeRef.current?.contentWindow || !code) { 40 | return; 41 | } 42 | 43 | const iframeWindow = iframeRef.current.contentWindow; 44 | 45 | iframeWindow.location.reload(); 46 | 47 | return new Promise((resolve) => { 48 | iframeRef.current!.onload = () => { 49 | const scope = new WeakMap(); 50 | 51 | // stub the iframe console 52 | (iframeWindow as any).console = consoleStub((type, ...args) => { 53 | dispatch({ 54 | type: "SET_LOGS", 55 | payload: { 56 | logs: [{ type, args, scope }], 57 | }, 58 | }); 59 | }); 60 | 61 | // stub the iframe Promise and Proxy 62 | (iframeWindow as any).Promise = promiseStub( 63 | scope, 64 | (iframeWindow as any).Promise 65 | ); 66 | (iframeWindow as any).Proxy = proxyStub( 67 | scope, 68 | (iframeWindow as any).Proxy 69 | ); 70 | 71 | // listen for errors in the iframe 72 | iframeWindow.addEventListener("error", (e: ErrorEvent) => { 73 | e.preventDefault(); 74 | dispatch({ 75 | type: "SET_LOGS", 76 | payload: { 77 | logs: [{ type: "error", args: [new Error(e.error)], scope }], 78 | }, 79 | }); 80 | }); 81 | 82 | // create a script tag and append it to the iframe body 83 | const script = document.createElement("script"); 84 | script.text = code; 85 | iframeWindow.document.body.appendChild(script); 86 | 87 | setTimeout(resolve, 250); 88 | }; 89 | }); 90 | }, 91 | [iframeRef, dispatch] 92 | ); 93 | 94 | useEffect(() => { 95 | if (!state.isRunning) { 96 | return; 97 | } 98 | 99 | if (state.language === Languages.JS) { 100 | runCode(state.code)!.finally(() => dispatch({ type: "RUN_COMPLETE" })); 101 | return; 102 | } 103 | 104 | compileCode() 105 | .then((code) => runCode(code)) 106 | .finally(() => dispatch({ type: "RUN_COMPLETE" })); 107 | }, [ 108 | state.isRunning, 109 | state.language, 110 | state.code, 111 | compileCode, 112 | runCode, 113 | dispatch, 114 | ]); 115 | 116 | return ( 117 |