diff --git a/.github/workflows/build_n_test.yml b/.github/workflows/build_n_test.yml
new file mode 100644
index 0000000..87c4597
--- /dev/null
+++ b/.github/workflows/build_n_test.yml
@@ -0,0 +1,45 @@
+name: "Build & Test"
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: ['*']
+ workflow_dispatch:
+
+jobs:
+ start:
+ name: Build & Test (Node.JS v${{ matrix.node }}, Fastify v${{ matrix.fastify }})
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ node: [ 22, 21, 20, 19 ]
+ fastify: ['4.14.0', '4.14', '4.15', '4.16', '5']
+
+ steps:
+ - name: Basic (1/1) - Checkout Project
+ uses: actions/checkout@v4
+
+ - name: Node.JS (1/3) - Installing
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node }}
+
+ - name: Node.JS (2/3) - NPM Modules Installing
+ run: |
+ npm install
+
+ - name: Node.JS (3/3) - Specific Fastify Installing
+ run: |
+ npm install fastify@${{ matrix.fastify }}
+
+ # Устанавливаем и тестируем с Fastify 4.14
+ - name: Build (1/2) - TypeScript Compilation
+ run: npm run build
+
+ - name: Build (2/2) - Launch Test
+ run: |
+ npm run test
+
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..572406b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/node_modules
+package-lock.json
\ No newline at end of file
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..ba848f9
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,5 @@
+/src/**/*.ts
+/dist/test/**
+/info/**
+tsconfig.json
+.github
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ad34619
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2024
+https://github.com/bsnext
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..55a7ea4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,66 @@
+# Fastify-Bowser
+![Build & Test](https://github.com/bsnext/fastify-bowser/actions/workflows/build_n_test.yml/badge.svg)
+![Node.JS Supported](https://badgen.net/static/Node.JS/%3E=19.0.0/green)
+![Fastify Supported](https://badgen.net/static/Fastify/%3E=14/green)
+![Install Size](https://badgen.net/packagephobia/install/@bsnext/fastify-bowser)
+![Dependencies](https://badgen.net/bundlephobia/dependency-count/@bsnext/fastify-bowser)
+![License](https://badgen.net/static/license/MIT/blue)
+
+The plugin adds the "request.useragent" property, which returns the parsed data.
+
+**Tested on Fastify v4.14+ and Node.JS v19+!**
+https://github.com/bsnext/fastify-bowser/actions/workflows/build_n_test.yml
+
+Why "[bowser](https://www.npmjs.com/package/bowser)", not "[ua-parser-js](https://www.npmjs.com/package/ua-parser-js)" or other library?
+Bowser it's a zero-dependency package with MIT license, unlike "ua-parser-js" under AGPL-3.0. Both of these libraries are good, but on top of that, bowser [is about x4 times more faster](https://github.com/bsnext/fastify-bowser/blob/master/info/benchmark.md).
+
+Why not [those package](https://github.com/Eomm/fastify-user-agent)? Under hood it have a "[useragent](https://www.npmjs.com/package/useragent)" - library with 2 dependencies. One of that is "LRU-Cache". But without cache it [have a performance less than both previously](https://github.com/bsnext/fastify-bowser/blob/master/info/benchmark.md) mentioned libraries. Also, **this library excludes parsing of user-agent until you call this property in request**.
+
+## Installing:
+```bash
+npm install @bsnext/fastify-bowser
+```
+
+## Usage:
+```ts
+import FastifyBowser from '@bsnext/fastify-bowser'; // TS
+import { default as FastifyBowser } from "@bsnext/fastify-bowser"; // MJS
+const { default: FastifyBowser } = require(`@bsnext/fastify-bowser`); // CJS
+
+const server = Fastify();
+await server.register(FastifyBowser, {
+ // Use parsed user-agent cache
+ cache: boolean = false;
+
+ // Cache limit. Will be automatically purged, if cache size reach limit.
+ cacheLimit: number = 100;
+
+ // Automatically cache purge interval in milliseconds.
+ cachePurgeTime: number = 60 * 5;
+});
+
+```
+
+## Example
+
+```ts
+import Fastify from 'fastify';
+import FastifyBowser from '@bsnext/fastify-bowser';
+
+const server = Fastify(...);
+await server.register(FastifyBowser, { cache: true });
+
+server.get(`/test`, function(request, response) {
+ response.send(request.useragent);
+
+ /* {
+ browser: { name: 'Chrome', version: '129.0.0.0' },
+ os: { name: 'Windows', version: 'NT 10.0', versionName: '10' },
+ platform: { type: 'desktop' },
+ engine: { name: 'Blink' }
+ } */
+})
+
+server.listen({ port: 8080 });
+
+```
\ No newline at end of file
diff --git a/dist/index.d.ts b/dist/index.d.ts
new file mode 100644
index 0000000..259d5cc
--- /dev/null
+++ b/dist/index.d.ts
@@ -0,0 +1,29 @@
+declare module "fastify" {
+ interface FastifyRequest {
+ useragent: {
+ ua: string;
+ browser: {
+ name: string | undefined;
+ version: string | undefined;
+ };
+ os: {
+ name: string | undefined;
+ version: string | undefined;
+ versionName: string | undefined;
+ };
+ platform: {
+ type: string | undefined;
+ };
+ engine: {
+ name: string | undefined;
+ };
+ };
+ }
+}
+export interface PluginOptions {
+ cache?: boolean;
+ cacheLimit?: number;
+ cachePurgeTime?: number;
+}
+declare const _default: (fastify: import("fastify").FastifyInstance, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>, initOptions: PluginOptions, done: (err?: Error) => void) => void;
+export default _default;
diff --git a/dist/index.js b/dist/index.js
new file mode 100644
index 0000000..06fd7b6
--- /dev/null
+++ b/dist/index.js
@@ -0,0 +1,63 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const fastify_plugin_1 = require("fastify-plugin");
+const Bowser = require("bowser");
+const undefinedUserAgentInfo = {
+ browser: { name: `''`, version: `''` },
+ os: {},
+ platform: {},
+ engine: {}
+};
+Object.freeze(undefinedUserAgentInfo.browser);
+Object.freeze(undefinedUserAgentInfo.os);
+Object.freeze(undefinedUserAgentInfo.platform);
+Object.freeze(undefinedUserAgentInfo.engine);
+Object.freeze(undefinedUserAgentInfo);
+exports.default = (0, fastify_plugin_1.default)(function (fastify, initOptions, done) {
+ if (initOptions.cache === true) {
+ if (initOptions.cacheLimit && ((typeof initOptions.cacheLimit !== `number`) || (initOptions.cacheLimit < 1))) {
+ console.warn(`Invalid parameter "cacheLimit" for @bsnext/fastify-bowser`);
+ }
+ if (initOptions.cachePurgeTime && ((typeof initOptions.cachePurgeTime !== `number`) || (initOptions.cachePurgeTime < 1))) {
+ console.warn(`Invalid parameter "cachePurgeTime" for @bsnext/fastify-bowser`);
+ }
+ }
+ const isCaching = initOptions.cache;
+ let cacheLimit = initOptions.cacheLimit || 100;
+ let cacheSize = 0;
+ let cachePool = Object.create(null);
+ function purgeCache() {
+ cachePool = Object.create(null);
+ cacheSize = 0;
+ }
+ if (isCaching) {
+ setInterval(purgeCache, (initOptions.cachePurgeTime || (60 * 5)) * 1000);
+ }
+ fastify.decorateRequest(`useragent`, {
+ getter() {
+ const userAgent = this.headers[`user-agent`];
+ if ((userAgent === undefined) || (userAgent === ``)) {
+ return undefinedUserAgentInfo;
+ }
+ const cachedQuery = cachePool[userAgent];
+ if (isCaching && cachedQuery) {
+ return cachedQuery;
+ }
+ const uaParseResult = Bowser.parse(userAgent);
+ if (isCaching) {
+ if (cacheSize >= cacheLimit) {
+ purgeCache();
+ }
+ cacheSize = cacheSize + 1;
+ cachePool[userAgent] = uaParseResult;
+ }
+ ;
+ return uaParseResult;
+ }
+ });
+ done();
+}, {
+ fastify: `^4.x.x || ^5.x`,
+ name: `@bsnext/fastify-bowser`
+});
+//# sourceMappingURL=index.js.map
\ No newline at end of file
diff --git a/dist/index.js.map b/dist/index.js.map
new file mode 100644
index 0000000..387ce36
--- /dev/null
+++ b/dist/index.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/dist/test/index.d.ts b/dist/test/index.d.ts
new file mode 100644
index 0000000..cb0ff5c
--- /dev/null
+++ b/dist/test/index.d.ts
@@ -0,0 +1 @@
+export {};
diff --git a/dist/test/index.js b/dist/test/index.js
new file mode 100644
index 0000000..5c73daa
--- /dev/null
+++ b/dist/test/index.js
@@ -0,0 +1,65 @@
+"use strict";
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+ return new (P || (P = Promise))(function (resolve, reject) {
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
+ });
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+const uvu_1 = require("uvu");
+const assert = require("uvu/assert");
+const fastify_1 = require("fastify");
+const index_1 = require("../index");
+function prepareServer() {
+ return __awaiter(this, void 0, void 0, function* () {
+ const server = (0, fastify_1.default)({
+ logger: false,
+ ignoreTrailingSlash: true,
+ });
+ yield server.register(index_1.default);
+ server.get(`/test`, function (request, response) {
+ response.send({
+ test: true,
+ useragent: request.useragent
+ });
+ });
+ return new Promise(function (resolve, reject) {
+ server.listen({ port: 8080 }, function () {
+ return __awaiter(this, void 0, void 0, function* () {
+ resolve(server);
+ });
+ });
+ });
+ });
+}
+(0, uvu_1.test)('Test', () => __awaiter(void 0, void 0, void 0, function* () {
+ const server = yield prepareServer();
+ try {
+ const result_test = yield fetch(`http://127.0.0.1:8080/test`, {
+ method: `GET`,
+ headers: {
+ [`User-Agent`]: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36`
+ }
+ }).then((o) => __awaiter(void 0, void 0, void 0, function* () { return yield o.json(); }));
+ assert.equal(result_test, {
+ test: true,
+ useragent: {
+ browser: { name: 'Chrome', version: '129.0.0.0' },
+ os: { name: 'Windows', version: 'NT 10.0', versionName: '10' },
+ platform: { type: 'desktop' },
+ engine: { name: 'Blink' }
+ }
+ });
+ }
+ catch (e) {
+ throw e;
+ }
+ finally {
+ server.close();
+ }
+}));
+uvu_1.test.run();
+//# sourceMappingURL=index.js.map
\ No newline at end of file
diff --git a/dist/test/index.js.map b/dist/test/index.js.map
new file mode 100644
index 0000000..b9cbc29
--- /dev/null
+++ b/dist/test/index.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/test/index.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,6BAA2B;AAC3B,qCAAqC;AAIrC,qCAAmD;AACnD,oCAAuD;AAIvD,SAAe,aAAa;;QAC3B,MAAM,MAAM,GAAG,IAAA,iBAAO,EAAC;YACtB,MAAM,EAAE,KAAK;YACb,mBAAmB,EAAE,IAAI;SACzB,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,QAAQ,CAAC,eAAY,CAAC,CAAC;QAEpC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,UAAU,OAAO,EAAE,QAAQ;YAC9C,QAAQ,CAAC,IAAI,CAAC;gBACb,IAAI,EAAE,IAAI;gBACV,SAAS,EAAE,OAAO,CAAC,SAAS;aAC5B,CAAC,CAAC;QACJ,CAAC,CAAC,CAAC;QAEH,OAAO,IAAI,OAAO,CAAC,UAAU,OAAO,EAAE,MAAM;YAC3C,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAC3B;;oBACC,OAAO,CAAC,MAAM,CAAC,CAAC;gBACjB,CAAC;aAAA,CACD,CAAC;QACH,CAAC,CAAC,CAAC;IACJ,CAAC;CAAA;AAID,IAAA,UAAI,EAAC,MAAM,EAAE,GAAS,EAAE;IACvB,MAAM,MAAM,GAAG,MAAM,aAAa,EAAE,CAAC;IAErC,IAAI,CAAC;QACJ,MAAM,WAAW,GAAG,MAAM,KAAK,CAAC,4BAA4B,EAAE;YAC7D,MAAM,EAAE,KAAK;YACb,OAAO,EAAE;gBACR,CAAC,YAAY,CAAC,EAAE,iHAAiH;aACjI;SACD,CAAC,CAAC,IAAI,CAAC,CAAM,CAAC,EAAC,EAAE,kDAAC,OAAA,MAAM,CAAC,CAAC,IAAI,EAAE,CAAA,GAAA,CAAC,CAAC;QAEnC,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE;YACzB,IAAI,EAAE,IAAI;YACV,SAAS,EAAE;gBACV,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE;gBACjD,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,IAAI,EAAE;gBAC9D,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;gBAC7B,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;aACzB;SACD,CAAC,CAAC;IACJ,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACZ,MAAM,CAAC,CAAC;IACT,CAAC;YAAS,CAAC;QACV,MAAM,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;AACF,CAAC,CAAA,CAAC,CAAC;AAIH,UAAI,CAAC,GAAG,EAAE,CAAC"}
\ No newline at end of file
diff --git a/info/benchmark.md b/info/benchmark.md
new file mode 100644
index 0000000..b7a2703
--- /dev/null
+++ b/info/benchmark.md
@@ -0,0 +1,61 @@
+#### If you wanna try that, disable "LRU" in "useragent" for fair benchmark:
+```js
+// node_modules/useragent/index.js : line 510
+
+exports.lookup = function lookup(userAgent, jsAgent) {
+ var key = (userAgent || '')+(jsAgent || '');
+ return exports.parse(userAgent, jsAgent);
+};
+```
+
+#### Modules:
+```
+npm install ua-parser-js bowser useragent
+```
+
+#### Code:
+
+```ts
+import { UAParser } from "ua-parser-js";
+import * as Bowser from "bowser";
+import * as Useragent from "useragent";
+
+import * as Benchmark from "benchmark";
+
+const bench = new Benchmark.Suite();
+
+/////////////////////////////
+
+const uaParserInstance = new UAParser();
+
+/////////////////////////////
+
+bench.add(`bowser`, function () {
+ const result = Bowser.parse(`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36`);
+});
+
+bench.add(`ua-parser-js`, function () {
+ const result = uaParserInstance.setUA(`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36`).getResult();
+});
+
+bench.add(`useragent`, function () {
+ const result = Useragent.lookup(`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36`);
+});
+
+/////////////////////////////
+
+bench.on('cycle', function (e) {
+ console.log(e.target.toString());
+});
+
+bench.run();
+```
+
+#### Results:
+```js
+bowser x 188,346 ops/sec ±0.48% (93 runs sampled)
+ua-parser-js x 50,341 ops/sec ±0.70% (94 runs sampled)
+useragent x 34,391 ops/sec ±0.37% (93 runs sampled)
+
+// Node v22.8.0, Win 11, Ryzen 7 3800X 3.89 GHz, 32 GB RAM 3200 MHz
+```
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..bcb4036
--- /dev/null
+++ b/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@bsnext/fastify-bowser",
+ "version": "1.0.2",
+ "description": "A plugin for Fastify that adds the 'request.useragent' property to get header 'user-agent' parsed data.",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "scripts": {
+ "start": "node dist/index.js",
+ "build": "tsc --project tsconfig.json",
+ "watch": "tsc --watch",
+ "test": "node dist/test/index.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/bsnext/Fastify-Bowser"
+ },
+ "keywords": [
+ "fastify",
+ "plugin",
+ "useragent",
+ "ua",
+ "user",
+ "agent",
+ "parser",
+ "bowser"
+ ],
+ "bugs": {
+ "url": "https://github.com/bsnext/Fastify-Bowser/issues"
+ },
+ "homepage": "https://github.com/bsnext/Fastify-Bowser#readme",
+ "author": "SalwadoR",
+ "type": "commonjs",
+ "license": "MIT",
+ "devDependencies": {
+ "@types/node": "^22.7.5",
+ "fastify": "^4.14.0",
+ "typescript": "^5.6.3",
+ "uvu": "^0.5.6"
+ },
+ "dependencies": {
+ "bowser": "^2.11.0",
+ "fastify-plugin": "^5.0.1"
+ }
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..4e0eb60
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,119 @@
+import { FastifyRequest } from "fastify";
+import FastifyPlugin from "fastify-plugin";
+
+import * as Bowser from "bowser";
+
+////////////////////////////////
+
+declare module "fastify" {
+ interface FastifyRequest {
+ useragent: {
+ ua: string;
+ browser: {
+ name: string | undefined;
+ version: string | undefined;
+ };
+ os: {
+ name: string | undefined;
+ version: string | undefined;
+ versionName: string | undefined;
+ };
+ platform: {
+ type: string | undefined;
+ };
+ engine: {
+ name: string | undefined;
+ };
+ };
+ }
+}
+
+export interface PluginOptions {
+ cache?: boolean;
+ cacheLimit?: number;
+ cachePurgeTime?: number;
+}
+
+////////////////////////////////
+
+const undefinedUserAgentInfo = {
+ browser: { name: `''`, version: `''` },
+ os: {},
+ platform: {},
+ engine: {}
+};
+
+Object.freeze(undefinedUserAgentInfo.browser);
+Object.freeze(undefinedUserAgentInfo.os);
+Object.freeze(undefinedUserAgentInfo.platform);
+Object.freeze(undefinedUserAgentInfo.engine);
+Object.freeze(undefinedUserAgentInfo);
+
+////////////////////////////////
+
+export default FastifyPlugin(
+ function (fastify, initOptions: PluginOptions, done) {
+ if (initOptions.cache === true) {
+ if (initOptions.cacheLimit && ((typeof initOptions.cacheLimit !== `number`) || (initOptions.cacheLimit < 1))) {
+ console.warn(`Invalid parameter "cacheLimit" for @bsnext/fastify-bowser`);
+ }
+ if (initOptions.cachePurgeTime && ((typeof initOptions.cachePurgeTime !== `number`) || (initOptions.cachePurgeTime < 1))) {
+ console.warn(`Invalid parameter "cachePurgeTime" for @bsnext/fastify-bowser`);
+ }
+ }
+
+ ////////////////////////////////
+
+ const isCaching = initOptions.cache;
+ let cacheLimit = initOptions.cacheLimit || 100;
+ let cacheSize = 0;
+ let cachePool = Object.create(null);
+
+ function purgeCache() {
+ cachePool = Object.create(null);
+ cacheSize = 0;
+ }
+
+ if (isCaching) {
+ setInterval(purgeCache, (initOptions.cachePurgeTime || (60 * 5)) * 1000);
+ }
+
+ ////////////////////////////////
+
+ fastify.decorateRequest(`useragent`, {
+ getter() {
+ const userAgent = (this as FastifyRequest).headers[`user-agent`];
+
+ if ((userAgent === undefined) || (userAgent === ``)) {
+ return undefinedUserAgentInfo;
+ }
+
+ const cachedQuery = cachePool[userAgent];
+
+ if (isCaching && cachedQuery) {
+ return cachedQuery;
+ }
+
+ const uaParseResult = Bowser.parse(userAgent);
+
+ if (isCaching) {
+ if (cacheSize >= cacheLimit!) {
+ purgeCache();
+ }
+
+ cacheSize = cacheSize + 1;
+ cachePool[userAgent] = uaParseResult;
+ };
+
+
+ return uaParseResult;
+ }
+ });
+
+ done();
+ },
+ {
+ fastify: `^4.x.x || ^5.x`,
+ name: `@bsnext/fastify-bowser`
+ }
+);
\ No newline at end of file
diff --git a/src/test/index.ts b/src/test/index.ts
new file mode 100644
index 0000000..9b61cbe
--- /dev/null
+++ b/src/test/index.ts
@@ -0,0 +1,66 @@
+import { test } from 'uvu';
+import * as assert from 'uvu/assert';
+
+////////////////////////////////
+
+import Fastify, { FastifyInstance } from 'fastify';
+import BowserPlugin, { PluginOptions } from '../index';
+
+////////////////////////////////
+
+async function prepareServer(): Promise {
+ const server = Fastify({
+ logger: false,
+ ignoreTrailingSlash: true,
+ });
+
+ await server.register(BowserPlugin);
+
+ server.get(`/test`, function (request, response) {
+ response.send({
+ test: true,
+ useragent: request.useragent
+ });
+ });
+
+ return new Promise(function (resolve, reject) {
+ server.listen({ port: 8080 },
+ async function () {
+ resolve(server);
+ }
+ );
+ });
+}
+
+////////////////////////////////
+
+test('Test', async () => {
+ const server = await prepareServer();
+
+ try {
+ const result_test = await fetch(`http://127.0.0.1:8080/test`, {
+ method: `GET`,
+ headers: {
+ [`User-Agent`]: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36`
+ }
+ }).then(async o => await o.json());
+
+ assert.equal(result_test, {
+ test: true,
+ useragent: {
+ browser: { name: 'Chrome', version: '129.0.0.0' },
+ os: { name: 'Windows', version: 'NT 10.0', versionName: '10' },
+ platform: { type: 'desktop' },
+ engine: { name: 'Blink' }
+ }
+ });
+ } catch (e) {
+ throw e;
+ } finally {
+ server.close();
+ }
+});
+
+////////////////////////////////
+
+test.run();
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..e82fefc
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "ES2016",
+ "module": "commonjs",
+ "noImplicitAny": false,
+ "strictNullChecks": true,
+ "strictPropertyInitialization": false,
+ "removeComments": true,
+ "preserveConstEnums": true,
+ // "experimentalDecorators": true,
+ // "emitDecoratorMetadata": true,
+ "sourceMap": true,
+ "declaration": true,
+ "declarationMap": false,
+ "rootDir": "src",
+ "outDir": "dist",
+ "baseUrl": "./",
+ },
+ "include": [
+ "./src/**/**.ts",
+ ],
+ "exclude": [
+ "node_modules",
+ "dist",
+ ]
+}
\ No newline at end of file