api-ape 2.0.0 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +203 -124
- package/client/README.md +37 -30
- package/client/browser.js +10 -8
- package/client/connectSocket.js +662 -381
- package/client/index.js +171 -0
- package/client/transports/streaming.js +240 -0
- package/dist/ape.js +2 -699
- package/dist/ape.js.map +7 -0
- package/dist/api-ape.min.js +2 -0
- package/dist/api-ape.min.js.map +7 -0
- package/index.d.ts +71 -18
- package/package.json +50 -15
- package/server/README.md +99 -13
- package/server/lib/broadcast.js +25 -8
- package/server/lib/bun.js +122 -0
- package/server/lib/longPolling.js +226 -0
- package/server/lib/main.js +381 -38
- package/server/lib/wiring.js +19 -12
- package/server/lib/ws/adapters/bun.js +225 -0
- package/server/lib/ws/adapters/deno.js +186 -0
- package/server/lib/ws/frames.js +217 -0
- package/server/lib/ws/index.js +15 -0
- package/server/lib/ws/server.js +109 -0
- package/server/lib/ws/socket.js +222 -0
- package/server/lib/wsProvider.js +135 -0
- package/server/security/origin.js +16 -4
- package/server/socket/receive.js +14 -1
- package/server/socket/send.js +6 -6
- package/server/utils/deepRequire.js +25 -10
- package/server/utils/parseUserAgent.js +286 -0
- package/example/Bun/README.md +0 -74
- package/example/Bun/api/message.ts +0 -11
- package/example/Bun/index.html +0 -76
- package/example/Bun/package.json +0 -9
- package/example/Bun/server.ts +0 -59
- package/example/Bun/styles.css +0 -128
- package/example/ExpressJs/README.md +0 -95
- package/example/ExpressJs/api/message.js +0 -11
- package/example/ExpressJs/backend.js +0 -39
- package/example/ExpressJs/index.html +0 -88
- package/example/ExpressJs/package-lock.json +0 -834
- package/example/ExpressJs/package.json +0 -10
- package/example/ExpressJs/styles.css +0 -128
- package/example/NextJs/.dockerignore +0 -29
- package/example/NextJs/Dockerfile +0 -52
- package/example/NextJs/Dockerfile.dev +0 -27
- package/example/NextJs/README.md +0 -113
- package/example/NextJs/ape/client.js +0 -66
- package/example/NextJs/ape/embed.js +0 -12
- package/example/NextJs/ape/index.js +0 -23
- package/example/NextJs/ape/logic/chat.js +0 -62
- package/example/NextJs/ape/onConnect.js +0 -69
- package/example/NextJs/ape/onDisconnect.js +0 -13
- package/example/NextJs/ape/onError.js +0 -9
- package/example/NextJs/ape/onReceive.js +0 -15
- package/example/NextJs/ape/onSend.js +0 -15
- package/example/NextJs/api/message.js +0 -44
- package/example/NextJs/docker-compose.yml +0 -22
- package/example/NextJs/next-env.d.ts +0 -5
- package/example/NextJs/next.config.js +0 -8
- package/example/NextJs/package-lock.json +0 -6400
- package/example/NextJs/package.json +0 -24
- package/example/NextJs/pages/Info.tsx +0 -153
- package/example/NextJs/pages/_app.tsx +0 -6
- package/example/NextJs/pages/index.tsx +0 -275
- package/example/NextJs/public/favicon.ico +0 -0
- package/example/NextJs/public/vercel.svg +0 -4
- package/example/NextJs/server.js +0 -36
- package/example/NextJs/styles/Chat.module.css +0 -448
- package/example/NextJs/styles/Home.module.css +0 -129
- package/example/NextJs/styles/globals.css +0 -26
- package/example/NextJs/tsconfig.json +0 -20
- package/example/README.md +0 -117
- package/example/Vite/README.md +0 -68
- package/example/Vite/ape/client.ts +0 -66
- package/example/Vite/ape/onConnect.ts +0 -52
- package/example/Vite/api/message.ts +0 -57
- package/example/Vite/index.html +0 -16
- package/example/Vite/package.json +0 -19
- package/example/Vite/server.ts +0 -62
- package/example/Vite/src/App.vue +0 -170
- package/example/Vite/src/components/Info.vue +0 -352
- package/example/Vite/src/main.ts +0 -5
- package/example/Vite/src/style.css +0 -200
- package/example/Vite/src/vite-env.d.ts +0 -7
- package/example/Vite/vite.config.ts +0 -20
- package/todo.md +0 -85
- package/utils/jss.test.js +0 -261
- package/utils/messageHash.test.js +0 -56
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Robust User-Agent Parser
|
|
3
|
+
* Zero-dependency replacement for ua-parser-js
|
|
4
|
+
* Handles browsers, OS, devices, bots (including AI), and edge cases
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Browser detection patterns - ORDER MATTERS (specific before generic)
|
|
8
|
+
const BROWSERS = [
|
|
9
|
+
// AI Bots first (most specific)
|
|
10
|
+
{ name: 'ChatGPT-User', pattern: /ChatGPT-User\/?([\d.]*)/i },
|
|
11
|
+
{ name: 'GPTBot', pattern: /GPTBot\/?([\d.]*)/i },
|
|
12
|
+
{ name: 'OAI-SearchBot', pattern: /OAI-SearchBot\/?([\d.]*)/i },
|
|
13
|
+
{ name: 'ClaudeBot', pattern: /ClaudeBot\/?([\d.]*)/i },
|
|
14
|
+
{ name: 'Claude-User', pattern: /Claude-User\/?([\d.]*)/i },
|
|
15
|
+
{ name: 'Claude-SearchBot', pattern: /Claude-SearchBot\/?([\d.]*)/i },
|
|
16
|
+
{ name: 'Claude-Web', pattern: /Claude-Web\/?([\d.]*)/i },
|
|
17
|
+
{ name: 'PerplexityBot', pattern: /PerplexityBot\/?([\d.]*)/i },
|
|
18
|
+
{ name: 'Perplexity-User', pattern: /Perplexity-User\/?([\d.]*)/i },
|
|
19
|
+
{ name: 'Google-Extended', pattern: /Google-Extended/i },
|
|
20
|
+
|
|
21
|
+
// Traditional bots
|
|
22
|
+
{ name: 'Googlebot', pattern: /Googlebot\/?([\d.]*)/i },
|
|
23
|
+
{ name: 'Bingbot', pattern: /bingbot\/?([\d.]*)/i },
|
|
24
|
+
{ name: 'YandexBot', pattern: /YandexBot\/?([\d.]*)/i },
|
|
25
|
+
{ name: 'DuckDuckBot', pattern: /DuckDuckBot\/?([\d.]*)/i },
|
|
26
|
+
{ name: 'Slurp', pattern: /Slurp/i },
|
|
27
|
+
{ name: 'Baiduspider', pattern: /Baiduspider\/?([\d.]*)/i },
|
|
28
|
+
{ name: 'curl', pattern: /curl\/?([\d.]*)/i },
|
|
29
|
+
{ name: 'wget', pattern: /Wget\/?([\d.]*)/i },
|
|
30
|
+
{ name: 'HeadlessChrome', pattern: /HeadlessChrome\/?([\d.]*)/i },
|
|
31
|
+
{ name: 'PhantomJS', pattern: /PhantomJS\/?([\d.]*)/i },
|
|
32
|
+
{ name: 'Puppeteer', pattern: /Puppeteer/i },
|
|
33
|
+
{ name: 'Playwright', pattern: /Playwright/i },
|
|
34
|
+
|
|
35
|
+
// WebViews / In-app browsers (before generic browsers)
|
|
36
|
+
{ name: 'Facebook', pattern: /\bFB[\w_]*\/?([\d.]*)/i },
|
|
37
|
+
{ name: 'Instagram', pattern: /Instagram\s?([\d.]*)/i },
|
|
38
|
+
{ name: 'Twitter', pattern: /Twitter/i },
|
|
39
|
+
{ name: 'TikTok', pattern: /TikTok/i },
|
|
40
|
+
{ name: 'Snapchat', pattern: /Snapchat/i },
|
|
41
|
+
{ name: 'LinkedIn', pattern: /LinkedInApp/i },
|
|
42
|
+
{ name: 'Pinterest', pattern: /Pinterest/i },
|
|
43
|
+
{ name: 'WhatsApp', pattern: /WhatsApp\/?([\d.]*)/i },
|
|
44
|
+
{ name: 'Telegram', pattern: /TelegramBot/i },
|
|
45
|
+
|
|
46
|
+
// Chromium-based (before Chrome)
|
|
47
|
+
{ name: 'Edge', pattern: /Edg(?:e|A|iOS)?\/?([\d.]*)/i },
|
|
48
|
+
{ name: 'Opera', pattern: /(?:OPR|Opera)\/?([\d.]*)/i },
|
|
49
|
+
{ name: 'Brave', pattern: /Brave\/?([\d.]*)/i },
|
|
50
|
+
{ name: 'Vivaldi', pattern: /Vivaldi\/?([\d.]*)/i },
|
|
51
|
+
{ name: 'Yandex', pattern: /YaBrowser\/?([\d.]*)/i },
|
|
52
|
+
{ name: 'Samsung Internet', pattern: /SamsungBrowser\/?([\d.]*)/i },
|
|
53
|
+
{ name: 'UC Browser', pattern: /UCBrowser\/?([\d.]*)/i },
|
|
54
|
+
{ name: 'QQ Browser', pattern: /QQBrowser\/?([\d.]*)/i },
|
|
55
|
+
{ name: 'Whale', pattern: /Whale\/?([\d.]*)/i },
|
|
56
|
+
|
|
57
|
+
// Major browsers
|
|
58
|
+
{ name: 'Chrome', pattern: /(?:Chrome|CriOS)\/?([\d.]*)/i },
|
|
59
|
+
{ name: 'Firefox', pattern: /(?:Firefox|FxiOS)\/?([\d.]*)/i },
|
|
60
|
+
{ name: 'Safari', pattern: /Version\/([\d.]*)\s.*Safari/i },
|
|
61
|
+
|
|
62
|
+
// Legacy IE
|
|
63
|
+
{ name: 'IE', pattern: /(?:MSIE\s|Trident.*rv:)([\d.]*)/i },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// OS detection patterns - ORDER MATTERS (specific before generic)
|
|
67
|
+
const OS_PATTERNS = [
|
|
68
|
+
{ name: 'iOS', pattern: /(?:iPhone|iPad|iPod).*?OS\s([\d_]+)/i, versionSep: '_' },
|
|
69
|
+
// Android - exclude "like Android" (e.g., Kindle UA)
|
|
70
|
+
{ name: 'Android', pattern: /(?<!like\s)Android\s?([\d.]*)/i },
|
|
71
|
+
{ name: 'macOS', pattern: /Mac OS X\s?([\d_\.]*)/i, versionSep: '_' },
|
|
72
|
+
{
|
|
73
|
+
name: 'Windows', pattern: /Windows NT\s?([\d.]*)/i, versionMap: {
|
|
74
|
+
'10.0': '10', '6.3': '8.1', '6.2': '8', '6.1': '7', '6.0': 'Vista', '5.1': 'XP'
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{ name: 'Chrome OS', pattern: /CrOS\s\w+\s([\d.]*)/i },
|
|
78
|
+
// Specific distros before generic Linux
|
|
79
|
+
{ name: 'Ubuntu', pattern: /Ubuntu/i },
|
|
80
|
+
{ name: 'Fedora', pattern: /Fedora/i },
|
|
81
|
+
{ name: 'FreeBSD', pattern: /FreeBSD/i },
|
|
82
|
+
{ name: 'Linux', pattern: /Linux/i },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// Engine detection patterns
|
|
86
|
+
const ENGINES = [
|
|
87
|
+
{ name: 'Blink', pattern: /Chrome\/([\d.]+)/i }, // Modern Chrome, Edge, Opera
|
|
88
|
+
{ name: 'Gecko', pattern: /Gecko\/([\d.]+)/i },
|
|
89
|
+
{ name: 'WebKit', pattern: /AppleWebKit\/([\d.]+)/i },
|
|
90
|
+
{ name: 'Trident', pattern: /Trident\/([\d.]+)/i },
|
|
91
|
+
{ name: 'EdgeHTML', pattern: /Edge\/([\d.]+)/i },
|
|
92
|
+
{ name: 'Presto', pattern: /Presto\/([\d.]+)/i },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// Device type patterns - ORDER MATTERS (console before tablet/mobile to catch Xbox/PlayStation first)
|
|
96
|
+
const DEVICE_PATTERNS = [
|
|
97
|
+
{ type: 'console', pattern: /PlayStation|Xbox|Nintendo/i },
|
|
98
|
+
{ type: 'tablet', pattern: /iPad|Android(?!.*Mobile)|Tablet|PlayBook/i },
|
|
99
|
+
{ type: 'mobile', pattern: /Mobile|Android.*Mobile|iPhone|iPod|BlackBerry|IEMobile|Opera Mini|Opera Mobi/i },
|
|
100
|
+
{ type: 'smarttv', pattern: /SmartTV|Smart-TV|GoogleTV|AppleTV|BRAVIA|WebOS|Tizen|HbbTV|NetCast/i },
|
|
101
|
+
{ type: 'wearable', pattern: /Watch|Fitbit/i },
|
|
102
|
+
{ type: 'embedded', pattern: /Embedded/i },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
// Device vendor/model patterns
|
|
106
|
+
const DEVICE_VENDORS = [
|
|
107
|
+
{ vendor: 'Apple', pattern: /iPhone|iPad|iPod|Macintosh|AppleTV/i },
|
|
108
|
+
{ vendor: 'Samsung', pattern: /Samsung|SM-|GT-/i },
|
|
109
|
+
{ vendor: 'Huawei', pattern: /Huawei|HUAWEI/i },
|
|
110
|
+
// Xiaomi: brand names + model codes (e.g., 24030PN60G, M2102J20SG)
|
|
111
|
+
{ vendor: 'Xiaomi', pattern: /Xiaomi|Mi\s|Redmi|\b\d{5}[A-Z]{2}\d{2}[A-Z]\b|\bM\d{4}[A-Z]\d{2}[A-Z]{2}\b/i },
|
|
112
|
+
{ vendor: 'Google', pattern: /Pixel|Nexus/i },
|
|
113
|
+
{ vendor: 'OnePlus', pattern: /OnePlus|ONEPLUS/i },
|
|
114
|
+
{ vendor: 'LG', pattern: /LG[-;\/\s]/i },
|
|
115
|
+
// Sony: brand + Xperia + tablet codes (SGP)
|
|
116
|
+
{ vendor: 'Sony', pattern: /Sony|Xperia|PlayStation|\bSGP\d+\b/i },
|
|
117
|
+
{ vendor: 'Motorola', pattern: /Motorola|Moto\s|\bmoto\s/i },
|
|
118
|
+
{ vendor: 'HTC', pattern: /HTC/i },
|
|
119
|
+
{ vendor: 'Nokia', pattern: /Nokia/i },
|
|
120
|
+
{ vendor: 'Oppo', pattern: /OPPO/i },
|
|
121
|
+
{ vendor: 'Vivo', pattern: /vivo/i },
|
|
122
|
+
{ vendor: 'Realme', pattern: /RMX\d/i },
|
|
123
|
+
// Microsoft: Xbox, Surface, and "Microsoft;" in UA
|
|
124
|
+
{ vendor: 'Microsoft', pattern: /Xbox|Surface|Microsoft;/i },
|
|
125
|
+
{ vendor: 'Nintendo', pattern: /Nintendo/i },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
// CPU architecture patterns
|
|
129
|
+
const CPU_PATTERNS = [
|
|
130
|
+
{ architecture: 'arm64', pattern: /aarch64|arm64/i },
|
|
131
|
+
{ architecture: 'arm', pattern: /arm(?!64)/i },
|
|
132
|
+
{ architecture: 'amd64', pattern: /x64|x86_64|amd64|Win64|WOW64/i },
|
|
133
|
+
{ architecture: 'ia32', pattern: /x86|i[36]86/i },
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
// Bot detection - comprehensive list
|
|
137
|
+
const BOT_PATTERNS = [
|
|
138
|
+
// AI bots
|
|
139
|
+
/ChatGPT|GPTBot|OAI-SearchBot|ClaudeBot|Claude-User|Claude-SearchBot|PerplexityBot|Perplexity-User|Google-Extended/i,
|
|
140
|
+
// Traditional search
|
|
141
|
+
/bot|crawl|spider|slurp|search|fetch|monitor|check|scan/i,
|
|
142
|
+
// Specific bots
|
|
143
|
+
/Googlebot|Bingbot|YandexBot|DuckDuckBot|Baiduspider|Sogou|Exabot|facebot|ia_archiver/i,
|
|
144
|
+
// Tools
|
|
145
|
+
/curl|wget|python|java|perl|ruby|php|http|node|axios|got\//i,
|
|
146
|
+
// Headless
|
|
147
|
+
/HeadlessChrome|PhantomJS|Puppeteer|Playwright|Selenium|WebDriver/i,
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
// Model extraction patterns
|
|
151
|
+
const MODEL_PATTERNS = [
|
|
152
|
+
// Apple devices - including iPad16,3 format
|
|
153
|
+
{ pattern: /(iPad\d+,\d+|iPhone\d+,\d+|iPod\d+,\d+)/, extract: (m) => m[1].replace(/\d+,\d+/, '') },
|
|
154
|
+
{ pattern: /(iPhone|iPad|iPod)[\s;]/, extract: (m) => m[1] },
|
|
155
|
+
// Samsung
|
|
156
|
+
{ pattern: /(SM-[A-Z0-9]+|GT-[A-Z0-9]+)/i, extract: (m) => m[1] },
|
|
157
|
+
// Google Pixel
|
|
158
|
+
{ pattern: /(Pixel[\s]?\d*[a-z]?\s?(?:Pro|XL)?)/i, extract: (m) => m[1].trim() },
|
|
159
|
+
// Nexus
|
|
160
|
+
{ pattern: /(Nexus\s?\d+[a-z]?)/i, extract: (m) => m[1] },
|
|
161
|
+
// Generic Android - "Build/MODEL" or "; MODEL Build"
|
|
162
|
+
{ pattern: /;\s*([^;)]+)\s*Build\//i, extract: (m) => m[1].trim() },
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Parse a User-Agent string and extract browser, OS, device, and bot info
|
|
167
|
+
* @param {string|null|undefined} ua - The User-Agent string
|
|
168
|
+
* @returns {Object} Parsed results matching ua-parser-js structure
|
|
169
|
+
*/
|
|
170
|
+
function parseUserAgent(ua) {
|
|
171
|
+
// Handle null/undefined
|
|
172
|
+
if (ua == null || typeof ua !== 'string') {
|
|
173
|
+
return createEmptyResult(ua);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = {
|
|
177
|
+
browser: { name: null, version: null, major: null },
|
|
178
|
+
engine: { name: null, version: null },
|
|
179
|
+
os: { name: null, version: null },
|
|
180
|
+
device: { type: null, vendor: null, model: null },
|
|
181
|
+
cpu: { architecture: null },
|
|
182
|
+
isBot: false,
|
|
183
|
+
raw: ua,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Detect browser
|
|
187
|
+
for (const { name, pattern } of BROWSERS) {
|
|
188
|
+
const match = ua.match(pattern);
|
|
189
|
+
if (match) {
|
|
190
|
+
result.browser.name = name;
|
|
191
|
+
result.browser.version = match[1] || null;
|
|
192
|
+
result.browser.major = match[1] ? match[1].split('.')[0] : null;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Fallback: Safari without version
|
|
198
|
+
if (!result.browser.name && /Safari/i.test(ua) && !/Chrome/i.test(ua)) {
|
|
199
|
+
result.browser.name = 'Safari';
|
|
200
|
+
const safariMatch = ua.match(/Safari\/([\d.]+)/i);
|
|
201
|
+
result.browser.version = safariMatch ? safariMatch[1] : null;
|
|
202
|
+
result.browser.major = result.browser.version ? result.browser.version.split('.')[0] : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Detect engine
|
|
206
|
+
for (const { name, pattern } of ENGINES) {
|
|
207
|
+
const match = ua.match(pattern);
|
|
208
|
+
if (match) {
|
|
209
|
+
result.engine.name = name;
|
|
210
|
+
result.engine.version = match[1] || null;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Detect OS
|
|
216
|
+
for (const { name, pattern, versionSep, versionMap } of OS_PATTERNS) {
|
|
217
|
+
const match = ua.match(pattern);
|
|
218
|
+
if (match) {
|
|
219
|
+
result.os.name = name;
|
|
220
|
+
let version = match[1] || null;
|
|
221
|
+
if (version && versionSep) {
|
|
222
|
+
version = version.replace(new RegExp(versionSep, 'g'), '.');
|
|
223
|
+
}
|
|
224
|
+
if (version && versionMap && versionMap[version]) {
|
|
225
|
+
version = versionMap[version];
|
|
226
|
+
}
|
|
227
|
+
result.os.version = version;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Detect device type (check tablet before mobile due to iPad containing 'Mobile')
|
|
233
|
+
for (const { type, pattern } of DEVICE_PATTERNS) {
|
|
234
|
+
if (pattern.test(ua)) {
|
|
235
|
+
result.device.type = type;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Detect device vendor
|
|
241
|
+
for (const { vendor, pattern } of DEVICE_VENDORS) {
|
|
242
|
+
if (pattern.test(ua)) {
|
|
243
|
+
result.device.vendor = vendor;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Detect device model
|
|
249
|
+
for (const { pattern, extract } of MODEL_PATTERNS) {
|
|
250
|
+
const match = ua.match(pattern);
|
|
251
|
+
if (match) {
|
|
252
|
+
result.device.model = extract(match);
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Detect CPU architecture
|
|
258
|
+
for (const { architecture, pattern } of CPU_PATTERNS) {
|
|
259
|
+
if (pattern.test(ua)) {
|
|
260
|
+
result.cpu.architecture = architecture;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Detect bot
|
|
266
|
+
result.isBot = BOT_PATTERNS.some(pattern => pattern.test(ua));
|
|
267
|
+
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Create an empty result for null/undefined/empty UA
|
|
273
|
+
*/
|
|
274
|
+
function createEmptyResult(ua) {
|
|
275
|
+
return {
|
|
276
|
+
browser: { name: null, version: null, major: null },
|
|
277
|
+
engine: { name: null, version: null },
|
|
278
|
+
os: { name: null, version: null },
|
|
279
|
+
device: { type: null, vendor: null, model: null },
|
|
280
|
+
cpu: { architecture: null },
|
|
281
|
+
isBot: false,
|
|
282
|
+
raw: ua || null,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
module.exports = parseUserAgent;
|
package/example/Bun/README.md
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# 🦍 Bun — Example
|
|
2
|
-
|
|
3
|
-
A minimal real-time chat app using Bun's native HTTP server with api-ape.
|
|
4
|
-
|
|
5
|
-
## Quick Start
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
bun install
|
|
9
|
-
bun run start
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
Open http://localhost:3000 in multiple browser windows.
|
|
13
|
-
|
|
14
|
-
## Project Structure
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
Bun/
|
|
18
|
-
├── server.ts # Bun server with api-ape (TypeScript)
|
|
19
|
-
├── api/
|
|
20
|
-
│ └── message.ts # Broadcast to other clients
|
|
21
|
-
├── index.html # Chat UI
|
|
22
|
-
└── styles.css # Styling
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## How It Works
|
|
26
|
-
|
|
27
|
-
### Server (server.js)
|
|
28
|
-
|
|
29
|
-
```js
|
|
30
|
-
const ape = require('api-ape')
|
|
31
|
-
|
|
32
|
-
const server = Bun.serve({
|
|
33
|
-
port: 3000,
|
|
34
|
-
fetch(req) {
|
|
35
|
-
const url = new URL(req.url)
|
|
36
|
-
if (url.pathname === '/') {
|
|
37
|
-
return new Response(Bun.file('./index.html'))
|
|
38
|
-
}
|
|
39
|
-
return new Response('Not Found', { status: 404 })
|
|
40
|
-
}
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
// Pass Bun's server to api-ape
|
|
44
|
-
ape(server, {
|
|
45
|
-
where: 'api',
|
|
46
|
-
onConnent: (socket, req, send) => {
|
|
47
|
-
send('init', { history: [], users: ape.online() })
|
|
48
|
-
ape.broadcast('users', { count: ape.online() })
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
onDisconnent: () => ape.broadcast('users', { count: ape.online() })
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
})
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## Why Bun?
|
|
58
|
-
|
|
59
|
-
| Feature | Benefit |
|
|
60
|
-
|---------|---------|
|
|
61
|
-
| **No Express needed** | Bun has built-in HTTP server |
|
|
62
|
-
| **Fast startup** | Bun starts in milliseconds |
|
|
63
|
-
| **Native TypeScript** | No build step for TS files |
|
|
64
|
-
| **Smaller footprint** | Fewer dependencies |
|
|
65
|
-
|
|
66
|
-
## Key Concepts Demonstrated
|
|
67
|
-
|
|
68
|
-
| Concept | Example |
|
|
69
|
-
|---------|---------|
|
|
70
|
-
| Auto-wiring | `ape(server, { where: 'api' })` loads `api/*.js` |
|
|
71
|
-
| onConnect hook | `onConnent: (socket, req, send) => { ... }` |
|
|
72
|
-
| Push on connect | `send('init', { history, users })` |
|
|
73
|
-
| Broadcast all | `broadcast('users', { count })` |
|
|
74
|
-
| Broadcast others | `this.broadcastOthers('message', data)` |
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
const messages: Array<{ user: string; text: string }> = []
|
|
2
|
-
|
|
3
|
-
// Send message → broadcast to others
|
|
4
|
-
module.exports = function (this: any, data: { user: string; text: string }) {
|
|
5
|
-
messages.push(data)
|
|
6
|
-
this.broadcastOthers('message', data)
|
|
7
|
-
return data
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// Export messages for history
|
|
11
|
-
module.exports._messages = messages
|
package/example/Bun/index.html
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
3
|
-
|
|
4
|
-
<head>
|
|
5
|
-
<title>Chat - api-ape</title>
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
-
<link rel="stylesheet" href="/styles.css">
|
|
8
|
-
</head>
|
|
9
|
-
|
|
10
|
-
<body>
|
|
11
|
-
<div id="app">
|
|
12
|
-
<div class="chat-container">
|
|
13
|
-
<div class="header">
|
|
14
|
-
<span class="title">💬 Chat</span>
|
|
15
|
-
<span class="online-count">🟢 {{ users }} online</span>
|
|
16
|
-
<span class="user-badge">● {{ user }}</span>
|
|
17
|
-
</div>
|
|
18
|
-
<div class="messages">
|
|
19
|
-
<div v-if="messages.length === 0" class="empty-state">No messages yet...</div>
|
|
20
|
-
<div v-for="(m, i) in messages" :key="i" :class="['message', m.user === user ? 'mine' : '']">
|
|
21
|
-
<span class="username">{{ m.user }}</span>
|
|
22
|
-
<span class="text">{{ m.text }}</span>
|
|
23
|
-
</div>
|
|
24
|
-
</div>
|
|
25
|
-
<div class="input-area">
|
|
26
|
-
<input class="message-input" placeholder="Type a message..." v-model="input" @keydown.enter="send" autofocus>
|
|
27
|
-
</div>
|
|
28
|
-
</div>
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<script src="https://unpkg.com/vue@3"></script>
|
|
32
|
-
<script src="/api/ape.js"></script>
|
|
33
|
-
<script>
|
|
34
|
-
const { createApp, ref, onMounted } = Vue
|
|
35
|
-
|
|
36
|
-
createApp({
|
|
37
|
-
setup() {
|
|
38
|
-
const user = ref('User' + Math.random().toString(36).slice(2, 6))
|
|
39
|
-
const users = ref(0)
|
|
40
|
-
const messages = ref([])
|
|
41
|
-
const input = ref('')
|
|
42
|
-
|
|
43
|
-
onMounted(() => {
|
|
44
|
-
// Receive init with history + user count on connect
|
|
45
|
-
ape.on('init', ({ data }) => {
|
|
46
|
-
messages.value = data.history || []
|
|
47
|
-
users.value = data.users || 0
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
// Listen for user count updates
|
|
51
|
-
ape.on('users', ({ data }) => {
|
|
52
|
-
users.value = data.count
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
// Listen for new messages from others
|
|
56
|
-
ape.on('message', ({ data }) => {
|
|
57
|
-
messages.value.push(data)
|
|
58
|
-
})
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
function send() {
|
|
62
|
-
if (!input.value) return
|
|
63
|
-
const msg = { user: user.value, text: input.value }
|
|
64
|
-
ape.message(msg).then(() => {
|
|
65
|
-
messages.value.push(msg)
|
|
66
|
-
})
|
|
67
|
-
input.value = ''
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return { user, users, messages, input, send }
|
|
71
|
-
}
|
|
72
|
-
}).mount('#app')
|
|
73
|
-
</script>
|
|
74
|
-
</body>
|
|
75
|
-
|
|
76
|
-
</html>
|
package/example/Bun/package.json
DELETED
package/example/Bun/server.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bun server using api-ape library (TypeScript)
|
|
3
|
-
* Bun natively supports TypeScript - no build step needed!
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createServer, IncomingMessage, ServerResponse } from 'http'
|
|
7
|
-
import path from 'path'
|
|
8
|
-
import fs from 'fs'
|
|
9
|
-
import ape from 'api-ape'
|
|
10
|
-
|
|
11
|
-
const port = parseInt(process.env.PORT || '3000', 10)
|
|
12
|
-
|
|
13
|
-
// Create HTTP server
|
|
14
|
-
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
15
|
-
const url = new URL(req.url || '/', `http://localhost:${port}`)
|
|
16
|
-
|
|
17
|
-
if (url.pathname === '/') {
|
|
18
|
-
res.writeHead(200, { 'Content-Type': 'text/html' })
|
|
19
|
-
res.end(fs.readFileSync(path.join(__dirname, 'index.html')))
|
|
20
|
-
return
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (url.pathname === '/styles.css') {
|
|
24
|
-
res.writeHead(200, { 'Content-Type': 'text/css' })
|
|
25
|
-
res.end(fs.readFileSync(path.join(__dirname, 'styles.css')))
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
res.writeHead(404)
|
|
30
|
-
res.end('Not Found')
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
// Initialize api-ape
|
|
34
|
-
ape(server, {
|
|
35
|
-
where: 'api',
|
|
36
|
-
onConnent: (socket, req, send) => {
|
|
37
|
-
const messageModule = require('./api/message')
|
|
38
|
-
setTimeout(() => {
|
|
39
|
-
send('init', { history: messageModule._messages, users: ape.online() })
|
|
40
|
-
ape.broadcast('users', { count: ape.online() })
|
|
41
|
-
}, 100)
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
onDisconnent: () => ape.broadcast('users', { count: ape.online() })
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
server.listen(port, () => {
|
|
50
|
-
console.log(`
|
|
51
|
-
╔═══════════════════════════════════════════════════════╗
|
|
52
|
-
║ 🦍 api-ape Bun Example (TypeScript) ║
|
|
53
|
-
╠═══════════════════════════════════════════════════════╣
|
|
54
|
-
║ HTTP: http://localhost:${port}/ ║
|
|
55
|
-
║ WebSocket: ws://localhost:${port}/api/ape ║
|
|
56
|
-
║ Runtime: Bun 🥖 + TypeScript ║
|
|
57
|
-
╚═══════════════════════════════════════════════════════╝
|
|
58
|
-
`)
|
|
59
|
-
})
|
package/example/Bun/styles.css
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
* {
|
|
2
|
-
box-sizing: border-box;
|
|
3
|
-
margin: 0;
|
|
4
|
-
padding: 0;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
body {
|
|
8
|
-
min-height: 100vh;
|
|
9
|
-
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
|
10
|
-
color: #fff;
|
|
11
|
-
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;
|
|
12
|
-
display: flex;
|
|
13
|
-
justify-content: center;
|
|
14
|
-
padding: 2rem;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
#app {
|
|
18
|
-
max-width: 500px;
|
|
19
|
-
width: 100%;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.chat-container {
|
|
23
|
-
background: rgba(255, 255, 255, 0.05);
|
|
24
|
-
border-radius: 20px;
|
|
25
|
-
overflow: hidden;
|
|
26
|
-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.header {
|
|
30
|
-
display: flex;
|
|
31
|
-
justify-content: space-between;
|
|
32
|
-
align-items: center;
|
|
33
|
-
padding: 1rem 1.5rem;
|
|
34
|
-
background: rgba(255, 255, 255, 0.1);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.title {
|
|
38
|
-
font-size: 1.25rem;
|
|
39
|
-
font-weight: bold;
|
|
40
|
-
background: linear-gradient(90deg, #00d2ff, #3a7bd5);
|
|
41
|
-
-webkit-background-clip: text;
|
|
42
|
-
-webkit-text-fill-color: transparent;
|
|
43
|
-
background-clip: text;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.user-badge {
|
|
47
|
-
font-size: 0.85rem;
|
|
48
|
-
color: #0f0;
|
|
49
|
-
font-weight: bold;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.messages {
|
|
53
|
-
height: 350px;
|
|
54
|
-
overflow-y: auto;
|
|
55
|
-
padding: 1rem;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.messages::-webkit-scrollbar {
|
|
59
|
-
width: 6px;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.messages::-webkit-scrollbar-track {
|
|
63
|
-
background: rgba(255, 255, 255, 0.05);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.messages::-webkit-scrollbar-thumb {
|
|
67
|
-
background: rgba(255, 255, 255, 0.2);
|
|
68
|
-
border-radius: 3px;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.empty-state {
|
|
72
|
-
text-align: center;
|
|
73
|
-
color: #666;
|
|
74
|
-
margin-top: 140px;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.message {
|
|
78
|
-
display: flex;
|
|
79
|
-
flex-direction: column;
|
|
80
|
-
gap: 0.25rem;
|
|
81
|
-
padding: 0.75rem 1rem;
|
|
82
|
-
margin-bottom: 0.5rem;
|
|
83
|
-
background: rgba(255, 255, 255, 0.05);
|
|
84
|
-
border-radius: 12px;
|
|
85
|
-
border-left: 3px solid #3a7bd5;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.message.mine {
|
|
89
|
-
background: rgba(0, 210, 255, 0.15);
|
|
90
|
-
border-left-color: #00d2ff;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.message .username {
|
|
94
|
-
color: #00d2ff;
|
|
95
|
-
font-size: 0.85rem;
|
|
96
|
-
font-weight: bold;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.message .text {
|
|
100
|
-
color: #fff;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
.input-area {
|
|
104
|
-
display: flex;
|
|
105
|
-
gap: 0.5rem;
|
|
106
|
-
padding: 1rem;
|
|
107
|
-
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.message-input {
|
|
111
|
-
flex: 1;
|
|
112
|
-
padding: 0.75rem 1rem;
|
|
113
|
-
font-size: 1rem;
|
|
114
|
-
border: none;
|
|
115
|
-
border-radius: 50px;
|
|
116
|
-
background: rgba(255, 255, 255, 0.1);
|
|
117
|
-
color: #fff;
|
|
118
|
-
outline: none;
|
|
119
|
-
transition: background 0.2s;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
.message-input::placeholder {
|
|
123
|
-
color: rgba(255, 255, 255, 0.5);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
.message-input:focus {
|
|
127
|
-
background: rgba(255, 255, 255, 0.15);
|
|
128
|
-
}
|