├── .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 | 
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 |
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 | {`${elm.tagName.toLowerCase()}>`}
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 | {`${node.tagName.toLowerCase()}>`}
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 |
125 | );
126 | }
127 |
--------------------------------------------------------------------------------
/src/components/toolbar.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | font-size: 0.75rem;
3 | line-height: 10px;
4 | padding: 0.5rem 1rem;
5 | display: flex;
6 | flex-wrap: wrap;
7 | flex-direction: row;
8 | justify-content: space-between;
9 | border-top: 1px solid var(--border-color);
10 | }
11 |
12 | .container a {
13 | color: var(--text-color);
14 | text-decoration: none;
15 | }
16 |
17 | .container a > svg {
18 | margin: 0;
19 | vertical-align: text-top;
20 | }
21 |
22 | .container button {
23 | border: 0;
24 | line-height: 0;
25 | background: none;
26 | padding: 0;
27 | margin: 0;
28 | cursor: pointer;
29 | color: var(--text-color);
30 | }
31 |
32 | .container button > svg {
33 | margin: 0;
34 | vertical-align: text-top;
35 | }
36 |
37 | .icon {
38 | vertical-align: text-bottom;
39 | }
40 |
41 | .icon,
42 | .container a,
43 | .container select,
44 | .container button {
45 | margin-left: 0.5rem;
46 | }
47 |
48 | .container button:first-child,
49 | .container a:first-child,
50 | .icon:first-child {
51 | margin-left: 0;
52 | }
53 |
54 | .error,
55 | .warning {
56 | margin-right: 0.25rem;
57 | }
58 |
59 | .run {
60 | color: var(--color-ansi-green);
61 | }
62 |
63 | .warning {
64 | color: var(--color-ansi-yellow);
65 | }
66 |
67 | .error {
68 | color: var(--color-ansi-red);
69 | }
70 |
71 | .loading {
72 | animation-name: spin;
73 | animation-duration: 750ms;
74 | animation-iteration-count: infinite;
75 | animation-timing-function: linear;
76 | }
77 |
78 | .dropdown {
79 | position: relative;
80 | display: inline-block;
81 | background: transparent;
82 | color: inherit;
83 | font-size: inherit;
84 | line-height: inherit;
85 | border: 0;
86 | outline: none;
87 | top: -1px;
88 | }
89 |
90 | @keyframes spin {
91 | from {
92 | transform:rotate(0deg);
93 | }
94 | to {
95 | transform:rotate(360deg);
96 | }
97 | }
--------------------------------------------------------------------------------
/src/components/toolbar.tsx:
--------------------------------------------------------------------------------
1 | import { useConsole } from "../context/console";
2 | import styles from "./toolbar.module.css";
3 | import Icons from "./icons";
4 | import { Languages } from "../libs/constants";
5 |
6 | export default function Toolbar() {
7 | const [state, dispatch] = useConsole();
8 | const errors = state.logs.filter((log) => log.type === "error");
9 | const warnings = state.logs.filter((log) => log.type === "warn");
10 |
11 | const handleRunCode = () => dispatch({ type: "RUN_CODE" });
12 |
13 | const handleClearLogs = () => dispatch({ type: "CLEAR_LOGS" });
14 |
15 | const handleSetLanguage = ({
16 | target,
17 | }: React.ChangeEvent) => {
18 | dispatch({ type: "SET_LANGUAGE", payload: { language: target.value } });
19 | };
20 |
21 | return (
22 |
23 |
24 | {state.isRunning && (
25 |
30 | )}
31 | {!state.isRunning && (
32 |
43 | )}
44 |
52 |
53 |
54 |
60 |
61 |
62 |
67 | {errors.length}
68 |
73 | {warnings.length}
74 |
85 |
86 |
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/src/context/console.tsx:
--------------------------------------------------------------------------------
1 | import { useReducer, createContext, useContext } from "react";
2 | import { editor as monacoEditor, languages } from "monaco-editor";
3 |
4 | import type { Type } from "../libs/log";
5 | import { Languages } from "../libs/constants";
6 |
7 | export type IStandaloneCodeEditor = monacoEditor.IStandaloneCodeEditor;
8 | export type TypeScriptWorker = languages.typescript.TypeScriptWorker;
9 |
10 | export type State = {
11 | code: string;
12 | isRunning: boolean;
13 | editor: IStandaloneCodeEditor | null;
14 | language: Languages.TS | Languages.JS;
15 | logs: { type: Type; args: any[]; scope: WeakMap }[];
16 | };
17 |
18 | export type ActionType =
19 | | "SET_EDITOR"
20 | | "SET_CODE"
21 | | "SET_LOGS"
22 | | "SET_LANGUAGE"
23 | | "CLEAR_LOGS"
24 | | "RUN_CODE"
25 | | "RUN_COMPLETE";
26 |
27 | type Action = {
28 | payload: State;
29 | type: ActionType;
30 | };
31 |
32 | const ConsoleContext = createContext<[State, React.Dispatch]>([
33 | {} as any,
34 | () => {},
35 | ]);
36 |
37 | const reducer = (state: State, action: Action) => {
38 | const { type, payload } = action;
39 |
40 | switch (type) {
41 | case "SET_EDITOR":
42 | return { ...state, editor: payload.editor };
43 | case "SET_CODE":
44 | const code = payload.code?.trim() || "";
45 | return { ...state, code };
46 | case "SET_LANGUAGE":
47 | return { ...state, language: payload.language };
48 | case "SET_LOGS":
49 | return {
50 | ...state,
51 | logs: [...state.logs, ...payload.logs],
52 | };
53 | case "CLEAR_LOGS":
54 | return {
55 | ...state,
56 | logs: [],
57 | };
58 | case "RUN_CODE":
59 | return {
60 | ...state,
61 | isRunning: true,
62 | };
63 | case "RUN_COMPLETE":
64 | return {
65 | ...state,
66 | isRunning: false,
67 | };
68 | default:
69 | return state;
70 | }
71 | };
72 |
73 | export function ConsoleProvider({ children }: { children: React.ReactNode }) {
74 | const [state, dispatch] = useReducer(reducer, {
75 | logs: [],
76 | code: "",
77 | editor: null,
78 | isRunning: false,
79 | language: Languages.TS,
80 | });
81 |
82 | return (
83 |
84 | {children}
85 |
86 | );
87 | }
88 |
89 | export function useConsole() {
90 | const context = useContext(ConsoleContext);
91 |
92 | if (!context) {
93 | throw new Error("useConsole must be used within a ConsoleProvider");
94 | }
95 |
96 | return context;
97 | }
98 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --toolbar-height: 31px;
3 | --text-color: #abb2bf;
4 | --bg-color: #1e2227;
5 | --border-color: #3e4452;
6 | --highlight-color: #2c313c;
7 | --color-muted: #6b717d;
8 | --color-ansi-green: #8cc265;
9 | --color-ansi-cyan: #42b3c2;
10 | --color-ansi-blue: #4aa5f0;
11 | --color-ansi-bright-blue: #4dc4ff;
12 | --color-ansi-yellow: #d18f52;
13 | --color-ansi-bright-yellow: #f0a45d;
14 | --color-ansi-red: #e05561;
15 | --color-ansi-bright-red: #ff616e;
16 | --font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
17 | color-scheme: dark;
18 | }
19 |
20 | *,:before,:after {
21 | padding: 0;
22 | margin: 0;
23 | box-sizing: border-box;
24 | -webkit-tap-highlight-color: transparent
25 | }
26 |
27 | html {
28 | font-size: 16px;
29 | }
30 |
31 | body {
32 | max-width: 100vw;
33 | max-height: 100vh;
34 | overflow: hidden;
35 | line-height: 1.5;
36 | color: var(--text-color);
37 | font-family: var(--font-family);
38 | background-color: var(--bg-color);
39 | }
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./app";
4 | import "./index.css";
5 |
6 | const root = ReactDOM.createRoot(document.getElementById("root")!);
7 |
8 | root.render(
9 |
10 |
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/src/libs/constants.ts:
--------------------------------------------------------------------------------
1 | export enum Languages {
2 | JS = "javascript",
3 | TS = "typescript",
4 | }
5 |
6 | export enum LogTypes {
7 | INFO = "info",
8 | ERROR = "error",
9 | WARN = "warn",
10 | DEBUG = "debug",
11 | }
12 |
13 | export enum ValueTypes {
14 | STRING = "string",
15 | NUMBER = "number",
16 | BOOLEAN = "boolean",
17 | OBJECT = "object",
18 | ARRAY = "array",
19 | FUNCTION = "function",
20 | UNDEFINED = "undefined",
21 | NULL = "null",
22 | PROXY = "proxy",
23 | PROMISE = "promise",
24 | SYMBOL = "symbol",
25 | MAP = "map",
26 | SET = "set",
27 | ERROR = "error",
28 | DATE = "date",
29 | REGEXP = "regexp",
30 | HTML_ELEMENT = "html_element",
31 | }
32 |
33 | export enum NodeTypes {
34 | ELEMENT_NODE = Node.ELEMENT_NODE,
35 | ATTRIBUTE_NODE = Node.ATTRIBUTE_NODE,
36 | TEXT_NODE = Node.TEXT_NODE,
37 | CDATA_SECTION_NODE = Node.CDATA_SECTION_NODE,
38 | PROCESSING_INSTRUCTION_NODE = Node.PROCESSING_INSTRUCTION_NODE,
39 | COMMENT_NODE = Node.COMMENT_NODE,
40 | DOCUMENT_NODE = Node.DOCUMENT_NODE,
41 | DOCUMENT_FRAGMENT_NODE = Node.DOCUMENT_FRAGMENT_NODE,
42 | }
43 |
--------------------------------------------------------------------------------
/src/libs/editor.ts:
--------------------------------------------------------------------------------
1 | import { EditorProps } from "@monaco-editor/react";
2 |
3 | export const options: EditorProps["options"] = {
4 | automaticLayout: true,
5 | tabSize: 2,
6 | scrollBeyondLastLine: false,
7 | minimap: {
8 | enabled: false,
9 | },
10 | scrollbar: {
11 | vertical: "auto",
12 | },
13 | };
14 |
15 | export const darkTheme = {
16 | base: "vs-dark",
17 | inherit: true,
18 | rules: [],
19 | colors: {
20 | "activityBar.background": "#23272e",
21 | "activityBar.foreground": "#d7dae0",
22 | "activityBarBadge.background": "#4d78cc",
23 | "activityBarBadge.foreground": "#f8fafd",
24 | "badge.background": "#23272e",
25 | "button.background": "#404754",
26 | "button.secondaryBackground": "#30333d",
27 | "button.secondaryForeground": "#c0bdbd",
28 | "checkbox.border": "#404754",
29 | "debugToolBar.background": "#1e2227",
30 | descriptionForeground: "#abb2bf",
31 | "diffEditor.insertedTextBackground": "#00809b33",
32 | "dropdown.background": "#1e2227",
33 | "dropdown.border": "#1e2227",
34 | "editor.background": "#23272e",
35 | "editor.findMatchBackground": "#42557b",
36 | "editor.findMatchBorder": "#457dff",
37 | "editor.findMatchHighlightBackground": "#6199ff2f",
38 | "editor.foreground": "#abb2bf",
39 | "editorBracketHighlight.foreground1": "#d19a66",
40 | "editorBracketHighlight.foreground2": "#c678dd",
41 | "editorBracketHighlight.foreground3": "#56b6c2",
42 | "editorHoverWidget.highlightForeground": "#61afef",
43 | "editorInlayHint.foreground": "#abb2bf",
44 | "editorInlayHint.background": "#2c313c",
45 | "editor.lineHighlightBackground": "#2c313c",
46 | "editorLineNumber.activeForeground": "#abb2bf",
47 | "editorGutter.addedBackground": "#109868",
48 | "editorGutter.deletedBackground": "#9A353D",
49 | "editorGutter.modifiedBackground": "#948B60",
50 | "editorOverviewRuler.addedBackground": "#109868",
51 | "editorOverviewRuler.deletedBackground": "#9A353D",
52 | "editorOverviewRuler.modifiedBackground": "#948B60",
53 | "editor.selectionBackground": "#67769660",
54 | "editor.selectionHighlightBackground": "#ffffff10",
55 | "editor.selectionHighlightBorder": "#dddddd",
56 | "editor.wordHighlightBackground": "#d2e0ff2f",
57 | "editor.wordHighlightBorder": "#7f848e",
58 | "editor.wordHighlightStrongBackground": "#abb2bf26",
59 | "editor.wordHighlightStrongBorder": "#7f848e",
60 | "editorBracketMatch.background": "#515a6b",
61 | "editorBracketMatch.border": "#515a6b",
62 | "editorCursor.background": "#ffffffc9",
63 | "editorCursor.foreground": "#528bff",
64 | "editorError.foreground": "#c24038",
65 | "editorGroup.background": "#181a1f",
66 | "editorGroup.border": "#181a1f",
67 | "editorGroupHeader.tabsBackground": "#1e2227",
68 | "editorHoverWidget.background": "#1e2227",
69 | "editorHoverWidget.border": "#181a1f",
70 | "editorIndentGuide.activeBackground": "#c8c8c859",
71 | "editorIndentGuide.background": "#3b4048",
72 | "editorLineNumber.foreground": "#495162",
73 | "editorMarkerNavigation.background": "#1e2227",
74 | "editorRuler.foreground": "#abb2bf26",
75 | "editorSuggestWidget.background": "#1e2227",
76 | "editorSuggestWidget.border": "#181a1f",
77 | "editorSuggestWidget.selectedBackground": "#2c313a",
78 | "editorWarning.foreground": "#d19a66",
79 | "editorWhitespace.foreground": "#ffffff1d",
80 | "editorWidget.background": "#1e2227",
81 | focusBorder: "#3e4452",
82 | "gitDecoration.ignoredResourceForeground": "#636b78",
83 | "input.background": "#1d1f23",
84 | "input.foreground": "#abb2bf",
85 | "list.activeSelectionBackground": "#2c313a",
86 | "list.activeSelectionForeground": "#d7dae0",
87 | "list.focusBackground": "#323842",
88 | "list.focusForeground": "#f0f0f0",
89 | "list.highlightForeground": "#ecebeb",
90 | "list.hoverBackground": "#2c313a",
91 | "list.hoverForeground": "#abb2bf",
92 | "list.inactiveSelectionBackground": "#323842",
93 | "list.inactiveSelectionForeground": "#d7dae0",
94 | "list.warningForeground": "#d19a66",
95 | "menu.foreground": "#abb2bf",
96 | "menu.separatorBackground": "#343a45",
97 | "minimapGutter.addedBackground": "#109868",
98 | "minimapGutter.deletedBackground": "#9A353D",
99 | "minimapGutter.modifiedBackground": "#948B60",
100 | "panel.border": "#3e4452",
101 | "panelSectionHeader.background": "#1e2227",
102 | "peekViewEditor.background": "#1b1d23",
103 | "peekViewEditor.matchHighlightBackground": "#29244b",
104 | "peekViewResult.background": "#22262b",
105 | "scrollbar.shadow": "#23252c",
106 | "scrollbarSlider.activeBackground": "#747d9180",
107 | "scrollbarSlider.background": "#4e566660",
108 | "scrollbarSlider.hoverBackground": "#5a637580",
109 | "settings.focusedRowBackground": "#23272e",
110 | "settings.headerForeground": "#fff",
111 | "sideBar.background": "#1e2227",
112 | "sideBar.foreground": "#abb2bf",
113 | "sideBarSectionHeader.background": "#23272e",
114 | "sideBarSectionHeader.foreground": "#abb2bf",
115 | "statusBar.background": "#1e2227",
116 | "statusBar.debuggingBackground": "#cc6633",
117 | "statusBar.debuggingBorder": "#ff000000",
118 | "statusBar.debuggingForeground": "#ffffff",
119 | "statusBar.foreground": "#9da5b4",
120 | "statusBar.noFolderBackground": "#1e2227",
121 | "statusBarItem.remoteBackground": "#4d78cc",
122 | "statusBarItem.remoteForeground": "#f8fafd",
123 | "tab.activeBackground": "#23272e",
124 | "tab.activeBorder": "#b4b4b4",
125 | "tab.activeForeground": "#dcdcdc",
126 | "tab.border": "#181a1f",
127 | "tab.hoverBackground": "#323842",
128 | "tab.inactiveBackground": "#1e2227",
129 | "tab.unfocusedHoverBackground": "#323842",
130 | "terminal.ansiBlack": "#3f4451",
131 | "terminal.ansiBlue": "#4aa5f0",
132 | "terminal.ansiBrightBlack": "#4f5666",
133 | "terminal.ansiBrightBlue": "#4dc4ff",
134 | "terminal.ansiBrightCyan": "#4cd1e0",
135 | "terminal.ansiBrightGreen": "#a5e075",
136 | "terminal.ansiBrightMagenta": "#de73ff",
137 | "terminal.ansiBrightRed": "#ff616e",
138 | "terminal.ansiBrightWhite": "#e6e6e6",
139 | "terminal.ansiBrightYellow": "#f0a45d",
140 | "terminal.ansiCyan": "#42b3c2",
141 | "terminal.ansiGreen": "#8cc265",
142 | "terminal.ansiMagenta": "#c162de",
143 | "terminal.ansiRed": "#e05561",
144 | "terminal.ansiWhite": "#d7dae0",
145 | "terminal.ansiYellow": "#d18f52",
146 | "terminal.background": "#23272e",
147 | "terminal.border": "#3e4452",
148 | "terminal.foreground": "#abb2bf",
149 | "terminal.selectionBackground": "#abb2bf30",
150 | "textBlockQuote.background": "#2e3440",
151 | "textBlockQuote.border": "#4b5362",
152 | "textLink.foreground": "#61afef",
153 | "textPreformat.foreground": "#d19a66",
154 | "titleBar.activeBackground": "#23272e",
155 | "titleBar.activeForeground": "#9da5b4",
156 | "titleBar.inactiveBackground": "#23272e",
157 | "titleBar.inactiveForeground": "#6b717d",
158 | "tree.indentGuidesStroke": "#ffffff1d",
159 | "walkThrough.embeddedEditorBackground": "#2e3440",
160 | "welcomePage.buttonHoverBackground": "#404754",
161 | },
162 | };
163 |
--------------------------------------------------------------------------------
/src/libs/log.ts:
--------------------------------------------------------------------------------
1 | export const TYPES = ["log", "info", "warn", "debug", "error"];
2 | export type Type = (typeof TYPES)[number];
3 |
4 | export default function consoleStub(
5 | setLog: (type: Type, ...args: any[]) => void
6 | ) {
7 | return new Proxy(console, {
8 | get(target: typeof console, prop: keyof typeof console) {
9 | if (TYPES.includes(prop as Type)) {
10 | return (...args: any[]) => {
11 | setLog(prop as Type, ...args);
12 | (target as any)[prop](...args);
13 | };
14 | }
15 |
16 | return target[prop];
17 | },
18 | });
19 | }
20 |
21 | export function promiseStub(
22 | scope: WeakMap,
23 | originalPromise: PromiseConstructor
24 | ) {
25 | return function (callback: any) {
26 | const promise = new originalPromise(callback);
27 | scope.set(promise, { state: "pending", value: null });
28 |
29 | promise.then(
30 | (value) => {
31 | scope.set(promise, { state: "fulfilled", value: value });
32 | },
33 | (value) => {
34 | scope.set(promise, { state: "rejected", value: value });
35 | }
36 | );
37 |
38 | return promise;
39 | };
40 | }
41 |
42 | export function proxyStub(
43 | scope: WeakMap,
44 | originalProxy: ProxyConstructor
45 | ) {
46 | return function (target: T, handler: ProxyHandler) {
47 | const proxy = new originalProxy(target, handler);
48 | scope.set(proxy, { target, handler, isProxy: true });
49 | return proxy;
50 | };
51 | }
52 |
--------------------------------------------------------------------------------
/src/libs/utils.ts:
--------------------------------------------------------------------------------
1 | import { NodeTypes, ValueTypes } from "./constants";
2 |
3 | export function getType(val: any) {
4 | switch (true) {
5 | case val === null:
6 | return ValueTypes.NULL;
7 | case val === undefined:
8 | return ValueTypes.UNDEFINED;
9 | case typeof val === "string":
10 | return ValueTypes.STRING;
11 | case typeof val === "number":
12 | return ValueTypes.NUMBER;
13 | case typeof val === "boolean":
14 | return ValueTypes.BOOLEAN;
15 | case typeof val === "function":
16 | return ValueTypes.FUNCTION;
17 | case Array.isArray(val):
18 | return ValueTypes.ARRAY;
19 | case val?.constructor?.name === "Proxy":
20 | return ValueTypes.PROXY;
21 | case val?.constructor?.name === "Promise":
22 | return ValueTypes.PROMISE;
23 | case val?.constructor?.name === "Symbol":
24 | return ValueTypes.SYMBOL;
25 | case val?.constructor?.name === "Map":
26 | return ValueTypes.MAP;
27 | case val?.constructor?.name === "Set":
28 | return ValueTypes.SET;
29 | case val?.constructor?.name === "Error":
30 | return ValueTypes.ERROR;
31 | case val?.constructor?.name === "Date":
32 | return ValueTypes.DATE;
33 | case val?.constructor?.name === "RegExp":
34 | return ValueTypes.REGEXP;
35 | case Object.values(NodeTypes).includes(val?.nodeType):
36 | return ValueTypes.HTML_ELEMENT;
37 | default:
38 | return ValueTypes.OBJECT;
39 | }
40 | }
41 |
42 | export function parseFunction(func: Function) {
43 | const funcStr = func.toString();
44 | const body = funcStr.replace(/^(function|class)\s*/g, "");
45 | const symbol = funcStr.startsWith("class") ? "class" : "ƒ";
46 |
47 | return { body, symbol };
48 | }
49 |
50 | export function getObjectName(obj: Record): string {
51 | if (obj[Symbol.toStringTag as any]) {
52 | return obj[Symbol.toStringTag as any];
53 | }
54 |
55 | return obj.constructor.name;
56 | }
57 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------