├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets
├── 1280x800-2.png
├── 1280x800.png
├── 1400x560.png
└── 440x280.png
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── background.js
├── icon.png
├── icon.svg
├── index.html
└── manifest.json
├── src
├── App.tsx
├── components
│ ├── ConnectionPanel.tsx
│ ├── HistoryPanel
│ │ ├── MessageHistory.tsx
│ │ ├── Messages.tsx
│ │ ├── UrlHistory.tsx
│ │ └── index.tsx
│ ├── MessageHistory.tsx
│ └── MessageInput.tsx
├── hooks
│ ├── useStorageState.ts
│ ├── useTheme.ts
│ └── useWebSocket.ts
├── index.css
├── index.tsx
├── types
│ └── message.ts
└── utils
│ ├── formatters.ts
│ └── jsonFormatter.ts
├── tailwind.config.js
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 | build
132 | build.zip
133 | .DS_Store
134 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [0.0.2] - 2025-01-10
4 |
5 | ### Added
6 | - GitHub repository link with star count in the header
7 | - Version number display in the header
8 |
9 | ### Changed
10 | - Improved UI styling for protocol selector dropdown
11 | - Reduced font size in message input area for better readability
12 | - Removed default WebSocket/Socket.IO URLs for better security
13 | - Updated header layout with better alignment and spacing
14 | - Enhanced dropdown menu with modern styling and custom arrow
15 |
16 | ### Fixed
17 | - Protocol switching no longer automatically sets predefined URLs
18 | - Better handling of connection state when switching protocols
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 oslook
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 |
2 |

3 |
Websocket Client Extension
4 |
Quick WebSocket Client with Socket.IO
5 |
6 |
7 |
8 | # [ ⏬ Install to Chrome](https://chromewebstore.google.com/detail/quick-websocket-client-wi/enmpedlkjjjnhoehlkkghdjiloebecpn)
9 |
10 | A powerful and user-friendly WebSocket testing client for developers.
11 |
12 | ## Features
13 |
14 | - 🔌 Easy WebSocket Connection Management
15 | - Quick connect/disconnect with WebSocket endpoints
16 | - Connection status monitoring
17 | - Detailed connection state logging
18 |
19 | - 📝 Advanced Message Handling
20 | - Support for both text and binary messages
21 | - Built-in JSON formatting and validation
22 | - Message history with timestamp and direction
23 | - Save frequently used messages for quick access
24 |
25 | - 💾 Persistent Storage
26 | - Auto-save connection history
27 | - Save favorite messages for reuse
28 | - Quick access to recent connections
29 |
30 | - 🎨 Modern User Interface
31 | - Clean and intuitive design
32 | - Real-time message updates
33 | - Compact message display
34 | - Dark/Light theme support
35 |
36 | ## Perfect for
37 |
38 | - Web developers testing WebSocket APIs
39 | - Backend developers debugging WebSocket services
40 | - QA engineers validating WebSocket implementations
41 | - Anyone working with WebSocket-based applications
42 |
43 | ## How to Use
44 |
45 | 1. Enter your WebSocket URL (e.g., ws://localhost:8080)
46 | 2. Click "Connect" to establish connection
47 | 3. Send text or binary messages
48 | 4. View real-time responses
49 | 5. Save frequently used messages for quick access
50 | 6. Monitor connection status and history
51 |
52 | ## Technical Details
53 |
54 | - Built with React and TypeScript
55 | - Uses native WebSocket API
56 | - Supports all modern browsers
57 | - Zero external dependencies for core functionality
58 | - Secure local storage for saved data
59 |
60 | Perfect for developers who need a reliable, feature-rich WebSocket testing tool right in their browser.
61 |
62 |
63 | Test Websoket: wss://ws.postman-echo.com/raw
64 | Test Socket.IO: https://ws.postman-echo.com/socketio
65 |
66 |
--------------------------------------------------------------------------------
/assets/1280x800-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslook/quick-websocket-client/acabd84494d884d61b667d9a7542c6a6e38016d7/assets/1280x800-2.png
--------------------------------------------------------------------------------
/assets/1280x800.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslook/quick-websocket-client/acabd84494d884d61b667d9a7542c6a6e38016d7/assets/1280x800.png
--------------------------------------------------------------------------------
/assets/1400x560.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslook/quick-websocket-client/acabd84494d884d61b667d9a7542c6a6e38016d7/assets/1400x560.png
--------------------------------------------------------------------------------
/assets/440x280.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslook/quick-websocket-client/acabd84494d884d61b667d9a7542c6a6e38016d7/assets/440x280.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "websocket-client-extension",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "react-scripts start",
7 | "build": "INLINE_RUNTIME_CHUNK=false react-scripts build",
8 | "test": "react-scripts test",
9 | "eject": "react-scripts eject"
10 | },
11 | "dependencies": {
12 | "date-fns": "^4.1.0",
13 | "react": "^18.2.0",
14 | "react-dom": "^18.2.0",
15 | "react-icons": "^5.4.0",
16 | "react-scripts": "5.0.1",
17 | "socket.io-client": "^4.7.4",
18 | "typescript": "^4.9.5"
19 | },
20 | "devDependencies": {
21 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
22 | "@types/chrome": "^0.0.254",
23 | "@types/react": "^18.2.0",
24 | "@types/react-dom": "^18.2.0",
25 | "autoprefixer": "^10.4.14",
26 | "postcss": "^8.4.23",
27 | "tailwindcss": "^3.3.2"
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.2%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
--------------------------------------------------------------------------------
/public/background.js:
--------------------------------------------------------------------------------
1 | chrome.action.onClicked.addListener(() => {
2 | chrome.tabs.create({
3 | url: 'index.html'
4 | });
5 | });
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslook/quick-websocket-client/acabd84494d884d61b667d9a7542c6a6e38016d7/public/icon.png
--------------------------------------------------------------------------------
/public/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Quick WebSocket Client
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "Quick WebSocket Client with Socket.IO",
4 | "version": "0.0.2.1",
5 | "description": "Construct custom Web Socket requests and handle responses to directly test your Web Socket services,Includes Socket.IO support.",
6 | "action": {
7 | "default_icon": "icon.png"
8 | },
9 | "background": {
10 | "service_worker": "background.js",
11 | "type": "module"
12 | },
13 | "permissions": ["storage"],
14 | "icons": {
15 | "48": "icon.png",
16 | "128": "icon.png"
17 | }
18 | }
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useWebSocket } from './hooks/useWebSocket';
2 | import { useState, useEffect, useRef } from 'react';
3 | import ConnectionPanel from './components/ConnectionPanel';
4 | import MessageInput from './components/MessageInput';
5 | import MessageHistory from './components/MessageHistory';
6 | import HistoryPanel from './components/HistoryPanel';
7 | import { useStorageState } from './hooks/useStorageState';
8 | import { useTheme } from './hooks/useTheme';
9 | import { SavedMessage, ProtocolType } from './types/message';
10 |
11 | const App = () => {
12 | const [url, setUrl] = useState('');
13 | const [protocol, setProtocol] = useState('websocket');
14 | const [savedMessages, setSavedMessages] = useStorageState('savedMessages', []);
15 | const [subscribedEvents, setSubscribedEvents] = useState>(new Set());
16 | const unsubscribedEvents = useRef>(new Set());
17 | const allReceivedEvents = useRef>(new Set()); // Track all unique events for autocomplete
18 | const [githubStars, setGithubStars] = useState(null);
19 | const { isDark, toggleTheme } = useTheme();
20 |
21 | useEffect(() => {
22 | // Fetch GitHub stars
23 | fetch('https://api.github.com/repos/oslook/quick-websocket-client')
24 | .then(res => res.json())
25 | .then(data => setGithubStars(data.stargazers_count))
26 | .catch(console.error);
27 | }, []);
28 |
29 | const {
30 | isConnected,
31 | messages,
32 | error,
33 | connect,
34 | disconnect,
35 | sendMessage,
36 | clearMessages,
37 | } = useWebSocket();
38 |
39 | // Auto-subscribe to received events
40 | useEffect(() => {
41 | if (protocol === 'socket.io') {
42 | const newEvents = messages
43 | .filter(m => m.direction === 'received' && m.event)
44 | .map(m => m.event as string);
45 |
46 | // Add to all received events for autocomplete
47 | newEvents.forEach(event => allReceivedEvents.current.add(event));
48 |
49 | // Filter out unsubscribed events and add new ones
50 | const eventsToAdd = newEvents.filter(event => !unsubscribedEvents.current.has(event));
51 |
52 | if (eventsToAdd.length > 0) {
53 | setSubscribedEvents(prev => new Set([...prev, ...eventsToAdd]));
54 | }
55 | }
56 | }, [messages, protocol]);
57 |
58 | const handleSaveMessage = (message: SavedMessage) => {
59 | if (!savedMessages.some(m =>
60 | m.content === message.content &&
61 | m.type === message.type &&
62 | m.event === message.event
63 | )) {
64 | setSavedMessages([message, ...savedMessages]);
65 | }
66 | };
67 |
68 | const handleProtocolChange = (newProtocol: ProtocolType) => {
69 | if (isConnected) {
70 | disconnect();
71 | }
72 | setProtocol(newProtocol);
73 | // if (newProtocol === 'websocket') {
74 | // setUrl('wss://ws.postman-echo.com/raw');
75 | // } else {
76 | // setUrl('https://ws.postman-echo.com/socketio');
77 | // }
78 | // Clear all events when switching protocols
79 | setSubscribedEvents(new Set());
80 | unsubscribedEvents.current.clear();
81 | allReceivedEvents.current.clear();
82 | };
83 |
84 | const handleUnsubscribe = (event: string) => {
85 | setSubscribedEvents(prev => {
86 | const newSet = new Set(prev);
87 | newSet.delete(event);
88 | return newSet;
89 | });
90 | unsubscribedEvents.current.add(event);
91 | };
92 |
93 | const handleSubscribe = (event: string) => {
94 | if (!subscribedEvents.has(event)) {
95 | setSubscribedEvents(prev => new Set([...prev, event]));
96 | unsubscribedEvents.current.delete(event);
97 | }
98 | };
99 |
100 | // Get all available events for autocomplete
101 | const getAvailableEvents = () => {
102 | return Array.from(allReceivedEvents.current)
103 | .filter(event => !subscribedEvents.has(event));
104 | };
105 |
106 | return (
107 |
108 | {/* Left Sidebar */}
109 |
110 | {/* URL History */}
111 |
112 |
Connection History
113 | setUrl(url)}
115 | disabled={isConnected}
116 | />
117 |
118 | {/* Saved Messages */}
119 |
120 |
Saved Messages
121 | {
124 | setSavedMessages(savedMessages.filter((_, i) => i !== index));
125 | }}
126 | onSelect={(content: string, type: 'text' | 'binary', event?: string) => isConnected && sendMessage(content, type, event)}
127 | disabled={!isConnected}
128 | />
129 |
130 |
131 |
132 | {/* Main Content */}
133 |
134 |
135 |
136 |
137 |
Quick WebSocket Client
138 | Including Socket.IO support
139 |
140 |
141 |
142 |
v0.0.2.1
143 | {githubStars !== null && (
144 |
145 |
148 | {githubStars}
149 |
150 | )}
151 |
159 |
160 |
175 |
176 |
177 |
187 |
188 |
189 |
195 |
196 |
197 |
207 |
208 |
209 |
210 | );
211 | };
212 |
213 | export default App;
--------------------------------------------------------------------------------
/src/components/ConnectionPanel.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useStorageState } from '../hooks/useStorageState';
3 | import { ProtocolType } from '../types/message';
4 |
5 | type ConnectionPanelProps = {
6 | url: string;
7 | setUrl: (url: string) => void;
8 | onConnect: (url: string) => void;
9 | onDisconnect: () => void;
10 | isConnected: boolean;
11 | error: string | null;
12 | protocol: ProtocolType;
13 | handleProtocolChange: (protocol: ProtocolType) => void;
14 | };
15 |
16 | const ConnectionPanel = ({
17 | url,
18 | setUrl,
19 | onConnect,
20 | onDisconnect,
21 | isConnected,
22 | error,
23 | protocol,
24 | handleProtocolChange
25 | }: ConnectionPanelProps) => {
26 | const [urlHistory, setUrlHistory] = useStorageState('urlHistory', []);
27 |
28 | const handleConnect = () => {
29 | if (url.trim()) {
30 | onConnect(url.trim());
31 | if (!urlHistory.includes(url)) {
32 | setUrlHistory([url, ...urlHistory].slice(0, 10));
33 | }
34 | }
35 | };
36 |
37 | return (
38 |
39 |
40 |
49 | setUrl(e.target.value)}
53 | className="flex-1 px-4 py-2 border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-gray-50 dark:bg-gray-700 dark:text-gray-200"
54 | placeholder={protocol === 'websocket' ? "WebSocket URL (ws:// or wss://)" : "Socket.IO URL (http:// or https://)"}
55 | disabled={isConnected}
56 | />
57 |
67 |
68 | {error && (
69 |
70 | {error}
71 |
72 | )}
73 |
74 | );
75 | };
76 |
77 | export default ConnectionPanel;
--------------------------------------------------------------------------------
/src/components/HistoryPanel/MessageHistory.tsx:
--------------------------------------------------------------------------------
1 | import { useStorageState } from '../../hooks/useStorageState';
2 | import { SavedMessage } from '../../types/message';
3 |
4 | type MessageHistoryProps = {
5 | onSelect: (content: string, type: 'text' | 'binary', event?: string) => void;
6 | disabled?: boolean;
7 | messages: SavedMessage[];
8 | onDeleteMessage: (index: number) => void;
9 | };
10 |
11 | const MessageHistory = ({
12 | onSelect,
13 | disabled,
14 | messages,
15 | onDeleteMessage
16 | }: MessageHistoryProps) => {
17 | if (messages.length === 0) {
18 | return (
19 | No saved messages
20 | );
21 | }
22 |
23 | return (
24 |
25 | {messages.map((message, index) => (
26 |
30 |
41 |
49 |
50 | ))}
51 |
52 | );
53 | };
54 |
55 | export default MessageHistory;
--------------------------------------------------------------------------------
/src/components/HistoryPanel/Messages.tsx:
--------------------------------------------------------------------------------
1 | import { SavedMessage, MessageType } from '../../types/message';
2 |
3 | type MessagesProps = {
4 | messages: SavedMessage[];
5 | onDeleteMessage: (index: number) => void;
6 | onSelect: (content: string, type: MessageType, event?: string) => void;
7 | disabled: boolean;
8 | };
9 |
10 | const Messages = ({ messages, onDeleteMessage, onSelect, disabled }: MessagesProps) => {
11 | const handleSelect = (message: SavedMessage) => {
12 | if (!disabled) {
13 | onSelect(message.content, message.type, message.event);
14 | }
15 | };
16 |
17 | return (
18 |
19 | {messages.map((message, index) => (
20 |
24 |
25 | {message.event && (
26 | {message.event}
27 | )}
28 | {message.content}
29 |
30 |
31 |
38 |
44 |
45 |
46 | ))}
47 | {messages.length === 0 && (
48 |
49 | No saved messages
50 |
51 | )}
52 |
53 | );
54 | };
55 |
56 | export default Messages;
--------------------------------------------------------------------------------
/src/components/HistoryPanel/UrlHistory.tsx:
--------------------------------------------------------------------------------
1 | import { useStorageState } from '../../hooks/useStorageState';
2 |
3 | type UrlHistoryProps = {
4 | onSelect: (url: string) => void;
5 | disabled?: boolean;
6 | };
7 |
8 | const UrlHistory = ({ onSelect, disabled }: UrlHistoryProps) => {
9 | const [urlHistory, setUrlHistory] = useStorageState('urlHistory', []);
10 |
11 | const handleDelete = (urlToDelete: string) => {
12 | setUrlHistory(urlHistory.filter(url => url !== urlToDelete));
13 | };
14 |
15 | if (urlHistory.length === 0) {
16 | return (
17 | No connection history
18 | );
19 | }
20 |
21 | return (
22 |
23 | {urlHistory.map((url) => (
24 |
28 |
36 |
44 |
45 | ))}
46 |
47 | );
48 | };
49 |
50 | export default UrlHistory;
--------------------------------------------------------------------------------
/src/components/HistoryPanel/index.tsx:
--------------------------------------------------------------------------------
1 | import UrlHistory from './UrlHistory';
2 | import MessageHistory from './MessageHistory';
3 |
4 | const HistoryPanel = {
5 | Urls: UrlHistory,
6 | Messages: MessageHistory,
7 | };
8 |
9 | export default HistoryPanel;
--------------------------------------------------------------------------------
/src/components/MessageHistory.tsx:
--------------------------------------------------------------------------------
1 | import { Message } from '../types/message';
2 | import { format } from 'date-fns';
3 | import { useEffect, useRef, useState } from 'react';
4 | import { formatJSON, isJSON, formatHexDump } from '../utils/formatters';
5 |
6 | type MessageHistoryProps = {
7 | messages: Message[];
8 | onClear: () => void;
9 | subscribedEvents?: Set;
10 | protocol?: 'websocket' | 'socket.io';
11 | };
12 |
13 | type ViewMode = 'text' | 'json' | 'hex';
14 |
15 | type MessageItemProps = {
16 | message: Message;
17 | className: string;
18 | };
19 |
20 | const MessageItem = ({ message, className }: MessageItemProps) => {
21 | const [isExpanded, setIsExpanded] = useState(false);
22 | const [viewMode, setViewMode] = useState('text');
23 | const content = message.content;
24 | const isExpandable = content.includes('\n') || content.length > 80;
25 | const isJSONContent = isJSON(content);
26 |
27 | // Format one-line preview
28 | const getPreviewContent = () => {
29 | if (content.length <= 80) return content;
30 | return content.split('\n')[0].slice(0, 80) + '...';
31 | };
32 |
33 | // Format expanded content based on view mode
34 | const getExpandedContent = () => {
35 | switch (viewMode) {
36 | case 'json':
37 | return isJSONContent ? formatJSON(content) : 'Invalid JSON';
38 | case 'hex':
39 | return formatHexDump(content);
40 | default:
41 | return content;
42 | }
43 | };
44 |
45 | return (
46 |
47 |
isExpandable && setIsExpanded(!isExpanded)}
50 | >
51 |
52 | {format(message.timestamp, 'HH:mm:ss.SSS')}
53 |
54 |
55 | {formatMessage(message, getPreviewContent())}
56 |
57 | {isExpandable && (
58 |
67 | )}
68 |
69 |
70 | {isExpanded && (
71 |
72 |
73 |
79 | {isJSONContent && (
80 |
86 | )}
87 |
93 |
94 |
95 | {getExpandedContent()}
96 |
97 |
98 | )}
99 |
100 | );
101 | };
102 |
103 | const MessageHistory = ({ messages, onClear, subscribedEvents = new Set(), protocol = 'websocket' }: MessageHistoryProps) => {
104 | const messagesEndRef = useRef(null);
105 | const scrollContainerRef = useRef(null);
106 |
107 | // Filter messages based on subscribed events
108 | const filteredMessages = messages.filter(message => {
109 | // Always show system/connection messages
110 | if (message.type === 'connection') return true;
111 |
112 | // For Socket.IO, only show messages with subscribed events or sent messages
113 | if (protocol === 'socket.io' && message.event) {
114 | return message.direction === 'sent' || subscribedEvents.has(message.event);
115 | }
116 |
117 | // Show all WebSocket messages
118 | return true;
119 | });
120 |
121 | // Auto scroll to bottom when new messages arrive
122 | useEffect(() => {
123 | const scrollContainer = scrollContainerRef.current;
124 | if (scrollContainer) {
125 | const { scrollHeight, clientHeight, scrollTop } = scrollContainer;
126 | const isScrolledNearBottom = scrollHeight - clientHeight - scrollTop < 100;
127 |
128 | if (isScrolledNearBottom) {
129 | messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
130 | }
131 | }
132 | }, [filteredMessages]);
133 |
134 | const getMessageClass = (message: Message) => {
135 | if (message.type === 'connection') {
136 | switch (message.level) {
137 | case 'success':
138 | return 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 border-green-200 dark:border-green-800';
139 | case 'error':
140 | return 'bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 border-red-200 dark:border-red-800';
141 | case 'warning':
142 | return 'bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800';
143 | default:
144 | return 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800';
145 | }
146 | }
147 | return message.direction === 'sent'
148 | ? 'bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300 border-purple-200 dark:border-purple-800'
149 | : 'bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-700';
150 | };
151 |
152 | return (
153 |
154 |
155 |
Messages
156 |
162 |
163 |
164 | {filteredMessages.map((message, index) => (
165 |
170 | ))}
171 | {filteredMessages.length === 0 && (
172 |
173 | No messages yet
174 |
175 | )}
176 |
177 |
178 |
179 | );
180 | };
181 |
182 | const formatMessage = (message: Message, content: string) => {
183 | if (message.type === 'connection') {
184 | return content;
185 | }
186 |
187 | let prefix = '';
188 | if (message.direction === 'sent') {
189 | prefix = message.event
190 | ? `📤 [Emit: ${message.event}] `
191 | : '📤 [Sent] ';
192 | } else {
193 | prefix = message.event
194 | ? `📥 [Event: ${message.event}] `
195 | : '📥 [Received] ';
196 | }
197 |
198 | return prefix + content;
199 | };
200 |
201 | export default MessageHistory;
--------------------------------------------------------------------------------
/src/components/MessageInput.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useCallback, useEffect, useRef } from 'react';
2 | import { useStorageState } from '../hooks/useStorageState';
3 | import { formatJSON, isJSON } from '../utils/jsonFormatter';
4 | import { ProtocolType, SavedMessage, MessageType } from '../types/message';
5 |
6 | type MessageInputProps = {
7 | onSend: (content: string, type: MessageType, event?: string) => void;
8 | isConnected: boolean;
9 | onSaveMessage: (message: SavedMessage) => void;
10 | protocol: ProtocolType;
11 | subscribedEvents: Set;
12 | onSubscribe: (event: string) => void;
13 | onUnsubscribe: (event: string) => void;
14 | availableEvents?: string[];
15 | };
16 |
17 | const MessageInput = ({
18 | onSend,
19 | isConnected,
20 | onSaveMessage,
21 | protocol,
22 | subscribedEvents,
23 | onSubscribe,
24 | onUnsubscribe,
25 | availableEvents = []
26 | }: MessageInputProps) => {
27 | const [message, setMessage] = useState('');
28 | const [type, setType] = useState('text');
29 | const [event, setEvent] = useState('');
30 | const [showFormatted, setShowFormatted] = useState(false);
31 | const [newEvent, setNewEvent] = useState('');
32 | const [showSuggestions, setShowSuggestions] = useState(false);
33 | const suggestionRef = useRef(null);
34 | const inputRef = useRef(null);
35 |
36 | // Handle click outside to close suggestions
37 | useEffect(() => {
38 | const handleClickOutside = (event: MouseEvent) => {
39 | if (suggestionRef.current && inputRef.current &&
40 | !suggestionRef.current.contains(event.target as Node) &&
41 | !inputRef.current.contains(event.target as Node)) {
42 | setShowSuggestions(false);
43 | }
44 | };
45 |
46 | document.addEventListener('mousedown', handleClickOutside);
47 | return () => {
48 | document.removeEventListener('mousedown', handleClickOutside);
49 | };
50 | }, []);
51 |
52 | // Filter suggestions based on input
53 | const suggestions = availableEvents
54 | .filter(e => e.toLowerCase().includes(newEvent.toLowerCase()))
55 | .slice(0, 5); // Limit to 5 suggestions
56 |
57 | const handleEventSelect = (selectedEvent: string) => {
58 | setNewEvent(selectedEvent);
59 | setShowSuggestions(false);
60 | // Optional: Auto subscribe when selecting an event
61 | if (selectedEvent.trim()) {
62 | onSubscribe(selectedEvent.trim());
63 | setNewEvent('');
64 | }
65 | };
66 |
67 | const handleSave = useCallback(() => {
68 | if (message.trim() && (type === 'text' || type === 'binary')) {
69 | onSaveMessage({
70 | content: message.trim(),
71 | type,
72 | event: protocol === 'socket.io' ? event : undefined
73 | });
74 | }
75 | }, [message, type, event, protocol, onSaveMessage]);
76 |
77 | const handleSend = useCallback(() => {
78 | if (message.trim() && isConnected) {
79 | onSend(message.trim(), type, protocol === 'socket.io' ? event : undefined);
80 | setMessage('');
81 | setShowFormatted(false);
82 | }
83 | }, [message, type, event, protocol, isConnected, onSend]);
84 |
85 | // Handle keyboard shortcuts
86 | useEffect(() => {
87 | const handleKeyDown = (e: KeyboardEvent) => {
88 | if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
89 | e.preventDefault();
90 | handleSend();
91 | }
92 | if ((e.metaKey || e.ctrlKey) && e.key === 's') {
93 | e.preventDefault();
94 | if (message.trim()) {
95 | handleSave();
96 | }
97 | }
98 | };
99 |
100 | document.addEventListener('keydown', handleKeyDown);
101 | return () => document.removeEventListener('keydown', handleKeyDown);
102 | }, [handleSend, handleSave, message]);
103 |
104 | const handleFormat = () => {
105 | if (isJSON(message)) {
106 | const formatted = formatJSON(message);
107 | if (formatted) {
108 | setMessage(formatted);
109 | setShowFormatted(true);
110 | }
111 | }
112 | };
113 |
114 | return (
115 |
116 |
117 | {protocol === 'socket.io' && (
118 |
119 |
setEvent(e.target.value)}
123 | className="flex-1 px-4 py-2 border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-gray-50 dark:bg-gray-700 dark:text-gray-200"
124 | placeholder="Event name"
125 | disabled={!isConnected}
126 | />
127 |
128 |
{
133 | setNewEvent(e.target.value);
134 | setShowSuggestions(true);
135 | }}
136 | onFocus={() => setShowSuggestions(true)}
137 | className="w-full px-4 py-2 border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-gray-50 dark:bg-gray-700 dark:text-gray-200"
138 | placeholder="Subscribe to event"
139 | disabled={!isConnected}
140 | />
141 | {showSuggestions && suggestions.length > 0 && (
142 |
146 | {suggestions.map((suggestion) => (
147 |
154 | ))}
155 |
156 | )}
157 |
158 |
171 |
172 | )}
173 |
174 | {protocol === 'socket.io' && subscribedEvents.size > 0 && (
175 |
176 | {Array.from(subscribedEvents).map((e) => (
177 |
181 | {e}
182 |
188 |
189 | ))}
190 |
191 | )}
192 |
193 |
194 |
205 |
206 |
207 |
208 |
217 | {type === 'text' && (
218 |
225 | )}
226 |
227 |
228 |
237 |
246 |
247 |
248 |
249 |
250 | );
251 | };
252 |
253 | export default MessageInput;
--------------------------------------------------------------------------------
/src/hooks/useStorageState.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | export const useStorageState = (key: string, initialValue: T) => {
4 | const [value, setValue] = useState(initialValue);
5 |
6 | useEffect(() => {
7 | const getStoredValue = async () => {
8 | try {
9 | const result = await chrome.storage.local.get([key]);
10 | if (result[key]) {
11 | setValue(result[key]);
12 | }
13 | } catch (error) {
14 | console.error('Failed to get stored value:', error);
15 | }
16 | };
17 |
18 | getStoredValue();
19 | }, [key]);
20 |
21 | const updateValue = async (newValue: T) => {
22 | try {
23 | setValue(newValue);
24 | await chrome.storage.local.set({ [key]: newValue });
25 | } catch (error) {
26 | console.error('Failed to update stored value:', error);
27 | }
28 | };
29 |
30 | return [value, updateValue] as const;
31 | };
--------------------------------------------------------------------------------
/src/hooks/useTheme.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | export const useTheme = () => {
4 | const [isDark, setIsDark] = useState(() => {
5 | const savedTheme = localStorage.getItem('theme');
6 | if (savedTheme) {
7 | return savedTheme === 'dark';
8 | }
9 | return window.matchMedia('(prefers-color-scheme: dark)').matches;
10 | });
11 |
12 | useEffect(() => {
13 | const root = window.document.documentElement;
14 | if (isDark) {
15 | root.classList.add('dark');
16 | localStorage.setItem('theme', 'dark');
17 | } else {
18 | root.classList.remove('dark');
19 | localStorage.setItem('theme', 'light');
20 | }
21 | }, [isDark]);
22 |
23 | const toggleTheme = () => setIsDark(!isDark);
24 |
25 | return { isDark, toggleTheme };
26 | };
--------------------------------------------------------------------------------
/src/hooks/useWebSocket.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback, useRef, useEffect } from 'react';
2 | import { Message, MessageType } from '../types/message';
3 | import { io, Socket } from 'socket.io-client';
4 |
5 | export const useWebSocket = () => {
6 | const [isConnected, setIsConnected] = useState(false);
7 | const [messages, setMessages] = useState([]);
8 | const [error, setError] = useState(null);
9 | const wsRef = useRef(null);
10 | const socketRef = useRef(null);
11 | const [protocol, setProtocol] = useState<'websocket' | 'socket.io'>('websocket');
12 |
13 | const addSystemMessage = (content: string, level: Message['level'] = 'info') => {
14 | setMessages(prev => [...prev, {
15 | content,
16 | type: 'connection',
17 | direction: 'system',
18 | timestamp: Date.now(),
19 | level
20 | }]);
21 | };
22 |
23 | const handleSocketIOMessage = (event: string, data: any) => {
24 | const newMessage: Message = {
25 | content: typeof data === 'object' ? JSON.stringify(data, null, 2) : String(data),
26 | type: 'text',
27 | direction: 'received',
28 | timestamp: Date.now(),
29 | event
30 | };
31 | setMessages(prev => [...prev, newMessage]);
32 | };
33 |
34 | const connect = useCallback((url: string) => {
35 | try {
36 | if (url.startsWith('ws://') || url.startsWith('wss://')) {
37 | setProtocol('websocket');
38 | addSystemMessage(`Connecting to WebSocket at ${url}...`);
39 | wsRef.current = new WebSocket(url);
40 |
41 | wsRef.current.onopen = () => {
42 | setIsConnected(true);
43 | setError(null);
44 | addSystemMessage('WebSocket connection established successfully', 'success');
45 | };
46 |
47 | wsRef.current.onclose = (event) => {
48 | setIsConnected(false);
49 | addSystemMessage(
50 | `WebSocket connection closed${event.wasClean ? ' cleanly' : ''} with code ${event.code}${
51 | event.reason ? `: ${event.reason}` : ''
52 | }`,
53 | event.wasClean ? 'info' : 'warning'
54 | );
55 | };
56 |
57 | wsRef.current.onerror = (event) => {
58 | setError('WebSocket error occurred');
59 | setIsConnected(false);
60 | addSystemMessage('WebSocket connection error occurred', 'error');
61 | };
62 |
63 | wsRef.current.onmessage = (event) => {
64 | if (event.data instanceof Blob) {
65 | addSystemMessage(`Received binary message (${event.data.size} bytes)`, 'info');
66 | }
67 |
68 | const newMessage: Message = {
69 | content: event.data instanceof Blob ? '[Binary Data]' : event.data,
70 | type: event.data instanceof Blob ? 'binary' : 'text',
71 | direction: 'received',
72 | timestamp: Date.now(),
73 | };
74 | setMessages(prev => [...prev, newMessage]);
75 | };
76 | } else {
77 | setProtocol('socket.io');
78 | addSystemMessage(`Initializing Socket.IO connection to ${url}...`);
79 |
80 | // Close existing connection if any
81 | if (socketRef.current) {
82 | socketRef.current.close();
83 | socketRef.current = null;
84 | }
85 |
86 | // For Socket.IO, use the URL as is (should be http:// or https://)
87 | socketRef.current = io(url, {
88 | transports: ['websocket'],
89 | reconnection: true,
90 | reconnectionAttempts: 3,
91 | reconnectionDelay: 1000,
92 | timeout: 5000,
93 | forceNew: true,
94 | autoConnect: false,
95 | });
96 |
97 | // Debug connection details
98 | addSystemMessage(`Socket.IO configuration:
99 | • URL: ${url}
100 | • Transport: websocket
101 | • Timeout: 5000ms
102 | • Max reconnection attempts: 3`, 'info');
103 |
104 | // Connect manually
105 | socketRef.current.connect();
106 |
107 | socketRef.current.on('connect', () => {
108 | setIsConnected(true);
109 | setError(null);
110 | addSystemMessage(`Socket.IO connected successfully
111 | • Socket ID: ${socketRef.current?.id}
112 | • Transport: ${socketRef.current?.io.engine.transport.name}`, 'success');
113 | });
114 |
115 | socketRef.current.on('connect_error', (error) => {
116 | const errorMsg = `Socket.IO connection error: ${error.message}`;
117 | console.error('Socket.IO connection error:', error);
118 | setError(errorMsg);
119 | setIsConnected(false);
120 | addSystemMessage(errorMsg, 'error');
121 | });
122 |
123 | socketRef.current.on('disconnect', (reason) => {
124 | setIsConnected(false);
125 | addSystemMessage(`Socket.IO disconnected: ${reason}`, 'warning');
126 | if (reason === 'io server disconnect') {
127 | addSystemMessage('Server initiated disconnect, attempting to reconnect...', 'info');
128 | socketRef.current?.connect();
129 | }
130 | });
131 |
132 | socketRef.current.on('reconnect_attempt', (attemptNumber) => {
133 | addSystemMessage(`Socket.IO reconnection attempt ${attemptNumber}/3...`, 'info');
134 | });
135 |
136 | socketRef.current.on('reconnect', (attemptNumber) => {
137 | addSystemMessage(`Socket.IO reconnected after ${attemptNumber} attempts`, 'success');
138 | setIsConnected(true);
139 | setError(null);
140 | });
141 |
142 | socketRef.current.on('reconnect_error', (error) => {
143 | addSystemMessage(`Socket.IO reconnection error: ${error.message}`, 'error');
144 | });
145 |
146 | socketRef.current.on('reconnect_failed', () => {
147 | addSystemMessage('Socket.IO reconnection failed after all attempts', 'error');
148 | });
149 |
150 | // Handle all incoming messages
151 | socketRef.current.onAny((event, ...args) => {
152 | if (event !== 'connect' && event !== 'disconnect' && !event.startsWith('reconnect')) {
153 | handleSocketIOMessage(event, args[0]);
154 | }
155 | });
156 | }
157 | } catch (err) {
158 | const errorMessage = err instanceof Error ? err.message : 'Failed to connect';
159 | console.error('Connection error:', err);
160 | setError(errorMessage);
161 | addSystemMessage(errorMessage, 'error');
162 | }
163 | }, []);
164 |
165 | const disconnect = useCallback(() => {
166 | addSystemMessage('Disconnecting...', 'info');
167 | if (protocol === 'websocket') {
168 | wsRef.current?.close();
169 | wsRef.current = null;
170 | } else {
171 | socketRef.current?.disconnect();
172 | socketRef.current = null;
173 | }
174 | setIsConnected(false);
175 | }, [protocol]);
176 |
177 | const sendMessage = useCallback((content: string, type: MessageType, event?: string) => {
178 | if (!isConnected) {
179 | setError('Not connected to server');
180 | addSystemMessage('Failed to send message: Not connected to server', 'error');
181 | return;
182 | }
183 |
184 | try {
185 | if (protocol === 'websocket') {
186 | if (!wsRef.current) {
187 | throw new Error('WebSocket not initialized');
188 | }
189 |
190 | if (type === 'binary') {
191 | const buffer = new TextEncoder().encode(content);
192 | wsRef.current.send(buffer);
193 | addSystemMessage(`Sent binary message (${buffer.length} bytes)`, 'info');
194 | } else {
195 | wsRef.current.send(content);
196 | }
197 | } else {
198 | if (!socketRef.current) {
199 | throw new Error('Socket.IO not initialized');
200 | }
201 |
202 | if (!event) {
203 | throw new Error('Event name is required for Socket.IO messages');
204 | }
205 |
206 | socketRef.current.emit(event, content);
207 | addSystemMessage(`Sent message to event: ${event}`, 'info');
208 | }
209 |
210 | const newMessage: Message = {
211 | content,
212 | type,
213 | direction: 'sent',
214 | timestamp: Date.now(),
215 | event: protocol === 'socket.io' ? event : undefined
216 | };
217 | setMessages(prev => [...prev, newMessage]);
218 | } catch (err) {
219 | const errorMessage = err instanceof Error ? err.message : 'Failed to send message';
220 | setError(errorMessage);
221 | addSystemMessage(errorMessage, 'error');
222 | }
223 | }, [isConnected, protocol]);
224 |
225 | // Cleanup on unmount
226 | useEffect(() => {
227 | return () => {
228 | if (wsRef.current) {
229 | wsRef.current.close();
230 | }
231 | if (socketRef.current) {
232 | socketRef.current.disconnect();
233 | }
234 | };
235 | }, []);
236 |
237 | return {
238 | isConnected,
239 | messages,
240 | error,
241 | connect,
242 | disconnect,
243 | sendMessage,
244 | clearMessages: useCallback(() => setMessages([]), []),
245 | };
246 | };
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | margin: 0;
7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
9 | sans-serif;
10 | -webkit-font-smoothing: antialiased;
11 | -moz-osx-font-smoothing: grayscale;
12 | }
13 |
14 | #root {
15 | width: 100%;
16 | height: 100%;
17 | }
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 |
6 | const container = document.getElementById('root');
7 | if (container) {
8 | const root = createRoot(container);
9 | root.render(
10 |
11 |
12 |
13 | );
14 | }
--------------------------------------------------------------------------------
/src/types/message.ts:
--------------------------------------------------------------------------------
1 | export type MessageDirection = 'sent' | 'received' | 'system';
2 | export type MessageType = 'text' | 'binary' | 'connection';
3 | export type ProtocolType = 'websocket' | 'socket.io';
4 |
5 | export interface Message {
6 | content: string;
7 | type: MessageType;
8 | direction: MessageDirection;
9 | timestamp: number;
10 | level?: 'info' | 'success' | 'error' | 'warning';
11 | event?: string; // For Socket.IO events
12 | }
13 |
14 | export interface SavedMessage {
15 | content: string;
16 | type: 'text' | 'binary';
17 | event?: string; // For Socket.IO events
18 | }
--------------------------------------------------------------------------------
/src/utils/formatters.ts:
--------------------------------------------------------------------------------
1 | export const formatJSON = (str: string): string | null => {
2 | try {
3 | const obj = JSON.parse(str);
4 | return JSON.stringify(obj, null, 2);
5 | } catch (e) {
6 | return null;
7 | }
8 | };
9 |
10 | export const isJSON = (str: string): boolean => {
11 | try {
12 | JSON.parse(str);
13 | return true;
14 | } catch (e) {
15 | return false;
16 | }
17 | };
18 |
19 | export const formatHexDump = (str: string): string => {
20 | const bytes = new TextEncoder().encode(str);
21 | const chunks: string[] = [];
22 | const bytesPerLine = 16;
23 |
24 | for (let i = 0; i < bytes.length; i += bytesPerLine) {
25 | const chunk = bytes.slice(i, i + bytesPerLine);
26 | const hex = Array.from(chunk)
27 | .map(byte => byte.toString(16).padStart(2, '0'))
28 | .join(' ');
29 | const ascii = Array.from(chunk)
30 | .map(byte => (byte >= 32 && byte <= 126) ? String.fromCharCode(byte) : '.')
31 | .join('');
32 |
33 | const offset = i.toString(16).padStart(8, '0');
34 | const hexPadded = hex.padEnd(bytesPerLine * 3 - 1, ' ');
35 | chunks.push(`${offset} ${hexPadded} |${ascii}|`);
36 | }
37 |
38 | return chunks.join('\n');
39 | };
--------------------------------------------------------------------------------
/src/utils/jsonFormatter.ts:
--------------------------------------------------------------------------------
1 | export const formatJSON = (str: string): string | null => {
2 | try {
3 | const obj = JSON.parse(str);
4 | return JSON.stringify(obj, null, 2);
5 | } catch (e) {
6 | return null;
7 | }
8 | };
9 |
10 | export const isJSON = (str: string): boolean => {
11 | try {
12 | JSON.parse(str);
13 | return true;
14 | } catch (e) {
15 | return false;
16 | }
17 | };
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ['./src/**/*.{js,jsx,ts,tsx}'],
3 | darkMode: 'class',
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | };
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 | "types": ["chrome"]
19 | },
20 | "include": ["src"]
21 | }
--------------------------------------------------------------------------------