socket.io first attempt

This commit is contained in:
NigeParis 2025-11-16 19:42:19 +01:00 committed by Maix0
parent 5a905a1239
commit cf6f3145b6
9 changed files with 4901 additions and 7 deletions

View file

@ -18,6 +18,7 @@
"@tailwindcss/vite": "^4.1.17",
"js-cookie": "^3.0.5",
"openapi-fetch": "^0.15.0",
"socket.io-client": "^4.8.1",
"tailwindcss": "^4.1.17"
}
}

View file

@ -17,6 +17,9 @@ importers:
openapi-fetch:
specifier: ^0.15.0
version: 0.15.0
socket.io-client:
specifier: ^4.8.1
version: 4.8.1
tailwindcss:
specifier: ^4.1.17
version: 4.1.17
@ -318,6 +321,9 @@ packages:
cpu: [x64]
os: [win32]
'@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
'@tailwindcss/node@4.1.17':
resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==}
@ -414,6 +420,15 @@ packages:
'@types/js-cookie@3.0.6':
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
debug@4.3.7:
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@ -427,6 +442,13 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
engine.io-client@6.6.3:
resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==}
engine.io-parser@5.2.3:
resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
engines: {node: '>=10.0.0'}
enhanced-resolve@5.18.3:
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
engines: {node: '>=10.13.0'}
@ -567,6 +589,14 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
socket.io-client@4.8.1:
resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==}
engines: {node: '>=10.0.0'}
socket.io-parser@4.2.4:
resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==}
engines: {node: '>=10.0.0'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@ -645,6 +675,22 @@ packages:
yaml:
optional: true
ws@8.17.1:
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
xmlhttprequest-ssl@2.1.2:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
engines: {node: '>=0.4.0'}
snapshots:
'@esbuild/aix-ppc64@0.25.12':
@ -810,6 +856,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.53.2':
optional: true
'@socket.io/component-emitter@3.1.2': {}
'@tailwindcss/node@4.1.17':
dependencies:
'@jridgewell/remapping': 2.3.5
@ -882,12 +930,30 @@ snapshots:
'@types/js-cookie@3.0.6': {}
debug@4.3.7:
dependencies:
ms: 2.1.3
debug@4.4.3:
dependencies:
ms: 2.1.3
detect-libc@2.1.2: {}
engine.io-client@6.6.3:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.3.7
engine.io-parser: 5.2.3
ws: 8.17.1
xmlhttprequest-ssl: 2.1.2
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
engine.io-parser@5.2.3: {}
enhanced-resolve@5.18.3:
dependencies:
graceful-fs: 4.2.11
@ -1038,6 +1104,24 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.53.2
fsevents: 2.3.3
socket.io-client@4.8.1:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.3.7
engine.io-client: 6.6.3
socket.io-parser: 4.2.4
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
socket.io-parser@4.2.4:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.3.7
transitivePeerDependencies:
- supports-color
source-map-js@1.2.1: {}
tailwindcss@4.1.17: {}
@ -1078,3 +1162,7 @@ snapshots:
fsevents: 2.3.3
jiti: 2.6.1
lightningcss: 1.30.2
ws@8.17.1: {}
xmlhttprequest-ssl@2.1.2: {}

View file

@ -4,16 +4,31 @@ import authHtml from './chat.html?raw';
import client from '@app/api'
import { updateUser } from "@app/auth";
import io from "socket.io-client"
const socket = io("https://localhost:8888");
// Listen for the 'connect' event
socket.on("connect", async() => {
console.log("Connected to the server: ", socket.id);
// Send a message to the server
socket.send("Hello from the client: " + `${socket.id}`);
// Emit a custom event 'coucou' with some data
socket.emit("coucou", { message: "Hello Nigel from coucou!" });
});
type Providers = {
name: string,
display_name: string,
icon_url?: string,
color?: { default: string, hover: string },
name: string,
display_name: string,
icon_url?: string,
color?: { default: string, hover: string },
};
function handleChat(_url: string, _args: RouteHandlerParams): RouteHandlerReturn {
setTitle('Chat Page');
// Listen for the 'connect' event
return {
html: authHtml, postInsert: async (app) => {
@ -78,7 +93,13 @@ function handleChat(_url: string, _args: RouteHandlerParams): RouteHandlerReturn
showError('Failed to login: Unknown error');
}
});
}
}
}
};
addRoute('/chat', handleChat, { bypass_auth: true });

View file

@ -1,6 +1,7 @@
import { updateUser } from '@app/auth';
import { route_404 } from './special_routes'
// ---- Router logic ----
export function navigateTo(url: string) {
if (url.startsWith('/') && !url.startsWith('/app'))

View file

@ -2,3 +2,17 @@
location /api/chat/ {
proxy_pass http://chat;
}
location /api/socket.io/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
root /volumes/static/chat;
proxy_pass http://chat$uri;
# proxy_pass http://chat;
}

4578
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -27,7 +27,8 @@
"@sinclair/typebox": "^0.34.41",
"fastify": "^5.6.2",
"fastify-cli": "^7.4.1",
"fastify-plugin": "^5.1.0"
"fastify-plugin": "^5.1.0",
"socket.io": "^4.8.1"
},
"devDependencies": {
"@types/node": "^22.19.1",

View file

@ -2,6 +2,57 @@ import { FastifyPluginAsync } from 'fastify';
import { MakeStaticResponse, typeResponse } from '@shared/utils';
import { Type } from '@sinclair/typebox';
import Fastify from 'fastify'
import { Server } from "socket.io"
// import path from 'path'
// import fastifyStatic from '@fastify/static'
// import fs from 'fs/promises'
// import { fileURLToPath } from 'url';
// import { dirname } from 'path';
// const __filename = fileURLToPath(import.meta.url);
// const __dirname = dirname(__filename);
// const fastifyWS = Fastify({
// logger: true
// })
// fastify.register(fastifyStatic, {
// root: path.join(__dirname, ''), prefix: '/'
// })
// const io = new Server(8888, {
// cors: {
// origin: "*",
// }
// })
const fastify = Fastify();
const io = new Server(fastify.server, { cors: { origin: "*" } });
import { Socket } from "socket.io";
io.on("connection", (socket: Socket) => {
console.log("testing")
console.log(`Client connected: ${socket.id}`);
socket.on("message", (data: any) => console.log(data, `socketID: ${socket.id}`));
socket.once("message", () => socket.send("connected succesfully"));
socket.once("coucou", (data: any) => console.log(data))
});
export const ChatRes = {
200: typeResponse('success', 'chat.success', {
name: Type.String(),

139
src/pnpm-lock.yaml generated
View file

@ -179,6 +179,9 @@ importers:
fastify-plugin:
specifier: ^5.1.0
version: 5.1.0
socket.io:
specifier: ^4.8.1
version: 4.8.1
devDependencies:
'@types/node':
specifier: ^22.19.1
@ -1094,6 +1097,9 @@ packages:
'@sinclair/typebox@0.34.41':
resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==}
'@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
'@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
@ -1113,6 +1119,9 @@ packages:
'@types/better-sqlite3@7.6.13':
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
'@types/cors@2.8.19':
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@ -1197,6 +1206,10 @@ packages:
abstract-logging@2.0.1:
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@ -1294,6 +1307,10 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
base64id@2.0.0:
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
engines: {node: ^4.5.0 || >= 5.9}
basic-ftp@5.0.5:
resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
engines: {node: '>=10.0.0'}
@ -1488,6 +1505,10 @@ packages:
core-js@3.46.0:
resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==}
cors@2.8.5:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@ -1512,6 +1533,15 @@ packages:
dateformat@4.6.3:
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
debug@4.3.7:
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@ -1597,6 +1627,14 @@ packages:
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
engine.io-parser@5.2.3:
resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
engines: {node: '>=10.0.0'}
engine.io@6.6.4:
resolution: {integrity: sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==}
engines: {node: '>=10.2.0'}
environment@1.1.0:
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
engines: {node: '>=18'}
@ -2387,6 +2425,10 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
@ -2871,6 +2913,17 @@ packages:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
socket.io-adapter@2.5.5:
resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==}
socket.io-parser@4.2.4:
resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==}
engines: {node: '>=10.0.0'}
socket.io@4.8.1:
resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==}
engines: {node: '>=10.2.0'}
socks-proxy-agent@8.0.5:
resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==}
engines: {node: '>= 14'}
@ -3247,6 +3300,18 @@ packages:
utf-8-validate:
optional: true
ws@8.17.1:
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
@ -4076,6 +4141,8 @@ snapshots:
'@sinclair/typebox@0.34.41': {}
'@socket.io/component-emitter@3.1.2': {}
'@standard-schema/spec@1.0.0': {}
'@tokenizer/inflate@0.3.1':
@ -4098,6 +4165,10 @@ snapshots:
dependencies:
'@types/node': 22.19.1
'@types/cors@2.8.19':
dependencies:
'@types/node': 22.19.1
'@types/estree@1.0.8': {}
'@types/json-schema@7.0.15': {}
@ -4214,6 +4285,11 @@ snapshots:
abstract-logging@2.0.1: {}
accepts@1.3.8:
dependencies:
mime-types: 2.1.35
negotiator: 0.6.3
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.15.0
@ -4303,6 +4379,8 @@ snapshots:
base64-js@1.5.1: {}
base64id@2.0.0: {}
basic-ftp@5.0.5: {}
bcrypt@6.0.0:
@ -4488,6 +4566,11 @@ snapshots:
core-js@3.46.0: {}
cors@2.8.5:
dependencies:
object-assign: 4.1.1
vary: 1.1.2
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@ -4510,6 +4593,10 @@ snapshots:
dateformat@4.6.3: {}
debug@4.3.7:
dependencies:
ms: 2.1.3
debug@4.4.3(supports-color@10.2.2):
dependencies:
ms: 2.1.3
@ -4585,6 +4672,24 @@ snapshots:
dependencies:
once: 1.4.0
engine.io-parser@5.2.3: {}
engine.io@6.6.4:
dependencies:
'@types/cors': 2.8.19
'@types/node': 22.19.1
accepts: 1.3.8
base64id: 2.0.0
cookie: 0.7.2
cors: 2.8.5
debug: 4.3.7
engine.io-parser: 5.2.3
ws: 8.17.1
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
environment@1.1.0: {}
es-define-property@1.0.1: {}
@ -5396,6 +5501,8 @@ snapshots:
natural-compare@1.4.0: {}
negotiator@0.6.3: {}
neo-async@2.6.2: {}
netmask@2.0.2: {}
@ -5996,6 +6103,36 @@ snapshots:
smart-buffer@4.2.0: {}
socket.io-adapter@2.5.5:
dependencies:
debug: 4.3.7
ws: 8.17.1
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
socket.io-parser@4.2.4:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.3.7
transitivePeerDependencies:
- supports-color
socket.io@4.8.1:
dependencies:
accepts: 1.3.8
base64id: 2.0.0
cors: 2.8.5
debug: 4.3.7
engine.io: 6.6.4
socket.io-adapter: 2.5.5
socket.io-parser: 4.2.4
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
socks-proxy-agent@8.0.5:
dependencies:
agent-base: 7.1.4
@ -6349,6 +6486,8 @@ snapshots:
ws@7.5.10: {}
ws@8.17.1: {}
ws@8.18.3: {}
xtend@4.0.2: {}