Skip to content

Commit acb67ea

Browse files
authored
feat(@dobsjs/dev): devtool (#52)
1 parent 07bc14e commit acb67ea

File tree

22 files changed

+1100
-11
lines changed

22 files changed

+1100
-11
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
"@types/encodeurl": "^1.0.3",
1717
"@types/mime-types": "^3.0.1",
1818
"@types/node": "^24.10.0",
19+
"@types/react": "^19.2.7",
20+
"@types/react-dom": "^19.2.3",
1921
"cross-spawn": "^7.0.6",
2022
"eslint": "^9.39.0",
2123
"eslint-config-prettier": "^10.1.8",
@@ -38,6 +40,9 @@
3840
"lint-fix": "eslint --fix --ext .js,.ts .",
3941
"test": "vitest run",
4042
"build": "tsdown",
43+
"build:ui": "yarn workspace @dobsjs/dev vite build",
44+
"dev": "tsdown --watch",
45+
"dev:ui": "yarn workspace @dobsjs/dev vite dev",
4146
"run-script": "node -r @swc-node/register",
4247
"publish": "lerna version && yarn build && lerna publish from-package"
4348
},

packages/dobs-dev/compiled-ui/index.html

Lines changed: 28 additions & 0 deletions
Large diffs are not rendered by default.

packages/dobs-dev/index.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Document</title>
7+
<script type="application/json" id="_config">
8+
{}
9+
</script>
10+
</head>
11+
<body>
12+
<div id="main"></div>
13+
<script type="module" src="/src/render.tsx"></script>
14+
</body>
15+
</html>

packages/dobs-dev/package.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "@dobsjs/dev",
3+
"version": "0.1.0-beta.3",
4+
"main": "./dist/index.js",
5+
"module": "./dist/index.mjs",
6+
"types": "./dist/index.d.ts",
7+
"repository": "https://github.com/zely-js/dobs",
8+
"description": "Devtool for dobs",
9+
"files": [
10+
"dist"
11+
],
12+
"exports": {
13+
".": {
14+
"import": {
15+
"types": "./dist/index.d.mts",
16+
"default": "./dist/index.mjs"
17+
},
18+
"require": {
19+
"types": "./dist/index.d.ts",
20+
"default": "./dist/index.js"
21+
}
22+
}
23+
},
24+
"devDependencies": {
25+
"@vitejs/plugin-react": "^5.1.1",
26+
"react": "^19.2.0",
27+
"react-dom": "^19.2.0",
28+
"sass": "^1.94.2",
29+
"vite": "^7.2.4",
30+
"vite-plugin-singlefile": "^2.3.0"
31+
}
32+
}

packages/dobs-dev/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { join } from 'node:path';
2+
3+
const path = join(__dirname, '../compiled-ui/index.html');
4+
5+
export { path, path as default };

packages/dobs-dev/src/render.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import reactDOM from 'react-dom/client';
2+
import App from './ui';
3+
4+
reactDOM.createRoot(document.getElementById('main') as any).render(<App></App>);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
@import url('https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,100..900&display=swap');
2+
3+
@import './variables';
4+
5+
* {
6+
box-sizing: border-box;
7+
}
8+
9+
body {
10+
margin: 0;
11+
background: var(--background);
12+
color: var(--foreground);
13+
font-family: 'Inter', sans-serif;
14+
}
15+
16+
button,
17+
input {
18+
font-family: 'Inter', sans-serif;
19+
}
20+
21+
.card {
22+
background: var(--card);
23+
border: 1px solid var(--border);
24+
border-radius: var(--radius);
25+
padding: 20px;
26+
max-width: 470px;
27+
overflow: auto;
28+
}
29+
30+
.button {
31+
padding: 10px 14px;
32+
background: var(--primary);
33+
color: var(--primary-foreground);
34+
border: none;
35+
border-radius: var(--radius);
36+
cursor: pointer;
37+
38+
&:disabled {
39+
opacity: 0.5;
40+
cursor: not-allowed;
41+
}
42+
}
43+
44+
.input {
45+
width: 100%;
46+
padding: 10px;
47+
border: 1px solid var(--border);
48+
border-radius: var(--radius);
49+
}
50+
51+
.textarea {
52+
width: 100%;
53+
min-height: 160px;
54+
padding: 10px;
55+
border: 1px solid var(--border);
56+
border-radius: var(--radius);
57+
}
58+
59+
.header {
60+
border-bottom: 1px solid var(--border);
61+
padding: 20px;
62+
background: var(--card);
63+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
:root {
2+
--background: oklch(0.99 0 0);
3+
--foreground: oklch(0.15 0 0);
4+
--primary: oklch(0.28 0 0);
5+
--primary-foreground: oklch(0.99 0 0);
6+
--border: oklch(0.9 0 0);
7+
--card: oklch(1 0 0);
8+
--card-foreground: oklch(0.15 0 0);
9+
10+
--radius: 8px;
11+
}
12+
13+
.dark {
14+
--background: oklch(0.11 0 0);
15+
--foreground: oklch(0.98 0 0);
16+
--primary: oklch(0.98 0 0);
17+
--primary-foreground: oklch(0.11 0 0);
18+
--border: oklch(0.24 0 0);
19+
--card: oklch(0.14 0 0);
20+
--card-foreground: oklch(0.98 0 0);
21+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { useState } from 'react';
2+
3+
interface Header {
4+
key: string;
5+
value: string;
6+
}
7+
8+
export default function RequestBuilder({ onResponse, isLoading, setIsLoading }) {
9+
const [method, setMethod] = useState('GET');
10+
const [url, setUrl] = useState('/');
11+
const [headers, setHeaders] = useState<Header[]>([
12+
{ key: 'Content-Type', value: 'application/json' },
13+
]);
14+
const [body, setBody] = useState(
15+
`{\n "title": "foo",\n "body": "bar",\n "userId": 1\n}`,
16+
);
17+
18+
const addHeader = () => setHeaders([...headers, { key: '', value: '' }]);
19+
const removeHeader = (i: number) => setHeaders(headers.filter((_, idx) => idx !== i));
20+
21+
const updateHeader = (index: number, field: 'key' | 'value', val: string) => {
22+
const newHeaders = [...headers];
23+
newHeaders[index][field] = val;
24+
setHeaders(newHeaders);
25+
};
26+
27+
const sendRequest = async () => {
28+
setIsLoading(true);
29+
const start = Date.now();
30+
31+
try {
32+
const headerObj = Object.fromEntries(
33+
headers.filter((h) => h.key && h.value).map((h) => [h.key, h.value]),
34+
);
35+
36+
const opts: RequestInit = {
37+
method,
38+
headers: headerObj,
39+
body: method !== 'GET' && method !== 'HEAD' ? body : undefined,
40+
};
41+
42+
const res = await fetch(url, opts);
43+
const end = Date.now();
44+
45+
const contentType = res.headers.get('content-type');
46+
const data = contentType?.includes('json') ? await res.json() : await res.text();
47+
48+
onResponse({
49+
status: res.status,
50+
statusText: res.statusText,
51+
headers: Object.fromEntries(res.headers.entries()),
52+
data,
53+
time: end - start,
54+
});
55+
} catch (err: any) {
56+
onResponse({
57+
status: 0,
58+
statusText: 'Error',
59+
headers: {},
60+
data: err.message,
61+
time: Date.now() - start,
62+
});
63+
} finally {
64+
setIsLoading(false);
65+
}
66+
};
67+
68+
const config = JSON.parse(document.getElementById('_config').textContent);
69+
70+
return (
71+
<div className="card">
72+
<div>
73+
<label>
74+
URL{' '}
75+
<span style={{ fontSize: '14px', opacity: 0.7 }}>
76+
(base : http://localhost:{config.port ?? 3000})
77+
</span>
78+
</label>
79+
<div style={{ display: 'flex', gap: '10px' }}>
80+
<select value={method} onChange={(e) => setMethod(e.target.value)}>
81+
{['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'].map((m) => (
82+
<option key={m}>{m}</option>
83+
))}
84+
</select>
85+
<input className="input" value={url} onChange={(e) => setUrl(e.target.value)} />
86+
</div>
87+
</div>
88+
89+
<h3>Headers</h3>
90+
91+
{headers.map((h, i) => (
92+
<div key={i} style={{ display: 'flex', gap: '10px', marginBottom: '8px' }}>
93+
<input
94+
className="input"
95+
placeholder="key"
96+
value={h.key}
97+
onChange={(e) => updateHeader(i, 'key', e.target.value)}
98+
/>
99+
<input
100+
className="input"
101+
placeholder="value"
102+
value={h.value}
103+
onChange={(e) => updateHeader(i, 'value', e.target.value)}
104+
/>
105+
<button className="button" onClick={() => removeHeader(i)}>
106+
X
107+
</button>
108+
</div>
109+
))}
110+
111+
<button className="button" onClick={addHeader}>
112+
Add Header
113+
</button>
114+
115+
<h3>Body</h3>
116+
<textarea
117+
className="textarea"
118+
disabled={method === 'GET' || method === 'HEAD'}
119+
value={body}
120+
onChange={(e) => setBody(e.target.value)}
121+
/>
122+
{(method === 'GET' || method === 'HEAD') && (
123+
<p style={{ fontSize: '12px', opacity: 0.7 }}>
124+
GET/HEAD requests cannot use a body.
125+
</p>
126+
)}
127+
128+
<button
129+
className="button"
130+
disabled={isLoading}
131+
onClick={sendRequest}
132+
style={{ width: '100%', marginTop: '15px' }}
133+
>
134+
{isLoading ? 'Sending...' : 'Send Request'}
135+
</button>
136+
</div>
137+
);
138+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export default function ResponseViewer({ response, isLoading }) {
2+
if (isLoading) {
3+
return <div className="card">Loading...</div>;
4+
}
5+
6+
if (!response) {
7+
return <div className="card">Waiting for request...</div>;
8+
}
9+
10+
return (
11+
<div className="card">
12+
<h3>Status</h3>
13+
<p>
14+
{response.status} {response.statusText}
15+
</p>
16+
17+
<h3>Time</h3>
18+
<p>{response.time} ms</p>
19+
20+
<h3>Headers</h3>
21+
<pre>{JSON.stringify(response.headers, null, 2)}</pre>
22+
23+
<h3>Body</h3>
24+
<pre>{JSON.stringify(response.data, null, 2)}</pre>
25+
</div>
26+
);
27+
}

0 commit comments

Comments
 (0)