user-agents 2.0.0-alpha.98 → 2.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "user-agents",
3
- "version": "2.0.0-alpha.98",
3
+ "version": "2.1.1",
4
4
  "description": "A JavaScript library for generating random user agents. ",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -19,6 +19,9 @@
19
19
  "import": "./dist/index.js",
20
20
  "require": "./dist/index.cjs"
21
21
  },
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
22
25
  "license": "BSD-2-Clause",
23
26
  "private": false,
24
27
  "type": "module",
@@ -26,8 +29,8 @@
26
29
  "build": "tsup",
27
30
  "postbuild": "yarn gunzip-data && cp src/user-agents.json dist/",
28
31
  "gunzip-data": "node --loader ts-node/esm src/gunzip-data.ts src/user-agents.json.gz",
29
- "lint": "eslint src/ && prettier --check src/",
30
- "test": "NODE_ENV=testing mocha --exit --require @babel/register --extensions '.ts, .js'",
32
+ "lint": "eslint src/ test/ && prettier --check src/ test/",
33
+ "test": "NODE_OPTIONS='--import tsx' mocha --exit --extensions '.ts, .js'",
31
34
  "update-data": "node --loader ts-node/esm src/update-data.ts src/user-agents.json.gz"
32
35
  },
33
36
  "devDependencies": {
@@ -35,39 +38,39 @@
35
38
  "@babel/core": "^7.23.2",
36
39
  "@babel/plugin-proposal-class-properties": "^7.18.6",
37
40
  "@babel/plugin-proposal-object-rest-spread": "^7.20.7",
38
- "@babel/plugin-syntax-import-assertions": "^7.22.5",
41
+ "@babel/plugin-syntax-import-attributes": "^7.22.5",
39
42
  "@babel/plugin-transform-classes": "^7.22.15",
40
43
  "@babel/preset-env": "^7.23.2",
41
44
  "@babel/preset-typescript": "^7.23.2",
42
45
  "@babel/register": "^7.22.15",
43
- "@types/lodash.clonedeep": "^4.5.8",
46
+ "@types/node": "^25.5.0",
44
47
  "@types/ua-parser-js": "^0.7.38",
45
- "@typescript-eslint/eslint-plugin": "^6.9.0",
46
- "@typescript-eslint/parser": "^6.9.0",
47
48
  "babel-loader": "^9.1.3",
48
49
  "babel-plugin-add-module-exports": "^1.0.4",
49
50
  "babel-preset-power-assert": "^3.0.0",
50
51
  "dynamoose": "^3.2.1",
51
- "esbuild": "^0.19.5",
52
- "eslint": "^8.52.0",
53
- "eslint-config-airbnb": "^19.0.4",
54
- "eslint-config-prettier": "^9.0.0",
55
- "eslint-plugin-import": "^2.29.0",
52
+ "esbuild": "^0.25.0",
53
+ "eslint": "^9",
54
+ "eslint-config-prettier": "^10.1.8",
56
55
  "fast-json-stable-stringify": "^2.1.0",
57
56
  "imports-loader": "^4.0.1",
58
57
  "isbot": "^3.7.0",
59
- "mocha": "^10.2.0",
58
+ "mocha": "^11.7.5",
60
59
  "power-assert": "^1.6.1",
61
- "prettier": "^3.0.3",
60
+ "prettier": "^3.1.0",
62
61
  "random": "^4.1.0",
63
62
  "source-map-support": "^0.5.21",
64
63
  "ts-node": "^10.9.1",
65
- "tsup": "^7.2.0",
66
- "typescript": "^5.2.2",
64
+ "tsup": "^8.5.0",
65
+ "tsx": "^4.21.0",
66
+ "typescript": "^5.8.0",
67
+ "typescript-eslint": "^8.57.0",
67
68
  "typescript-language-server": "^4.0.0",
68
69
  "ua-parser-js": "^1.0.36"
69
70
  },
70
- "dependencies": {
71
- "lodash.clonedeep": "^4.5.0"
71
+ "dependencies": {},
72
+ "resolutions": {
73
+ "diff": "^8.0.3",
74
+ "serialize-javascript": "^7.0.3"
72
75
  }
73
76
  }
@@ -1,4 +1,3 @@
1
- /* eslint-disable import/no-extraneous-dependencies */
2
1
  import fs from 'fs';
3
2
  import { argv } from 'process';
4
3
  import { fileURLToPath } from 'url';
@@ -67,6 +66,16 @@ const getUserAgentTable = async (limit = 1e4) => {
67
66
  // Filter out bots like Googlebot and YandexBot.
68
67
  if (isbot(profile.userAgent)) return;
69
68
 
69
+ // Strip extra quotes that some browsers include around the user agent string.
70
+ profile.userAgent = (profile.userAgent as string).replace(/^"|"$/g, '');
71
+
72
+ // Filter out spam/fake user agents with non-ASCII characters.
73
+ // Real browser UA strings are always pure ASCII per the HTTP spec.
74
+ if (/[^\x20-\x7E]/.test(profile.userAgent as string)) return;
75
+
76
+ // Filter out the literal `{userAgent}` placeholder string that some clients send.
77
+ if (profile.userAgent === '{userAgent}') return;
78
+
70
79
  // Track the counts for this exact profile.
71
80
  const stringifiedProfile = stableStringify(profile);
72
81
  if (!countsByProfile[stringifiedProfile]) {
@@ -91,7 +100,7 @@ const getUserAgentTable = async (limit = 1e4) => {
91
100
  // Accumulate the profiles and add/remove a few properties to match the historical format.
92
101
  const profiles: UserAgentData[] = [];
93
102
  Object.entries(countsByProfile).forEach(([stringifiedProfile, weight]) => {
94
- if (countsByProfile.hasOwnProperty(stringifiedProfile)) {
103
+ if (Object.hasOwn(countsByProfile, stringifiedProfile)) {
95
104
  const profile = JSON.parse(stringifiedProfile);
96
105
  profile.weight = weight;
97
106
  delete profile.sessionId;
@@ -136,7 +145,6 @@ if (fileURLToPath(import.meta.url) === argv[1]) {
136
145
  fs.writeFileSync(filename, content);
137
146
  })
138
147
  .catch((error) => {
139
- // eslint-disable-next-line no-console
140
148
  console.error(error);
141
149
  process.exit(1);
142
150
  });
package/src/user-agent.ts CHANGED
@@ -1,6 +1,4 @@
1
- import cloneDeep from 'lodash.clonedeep';
2
-
3
- import untypedUserAgents from './user-agents.json' assert { type: 'json' };
1
+ import untypedUserAgents from './user-agents.json' with { type: 'json' };
4
2
 
5
3
  const userAgents: UserAgentData[] = untypedUserAgents as UserAgentData[];
6
4
 
@@ -108,7 +106,7 @@ const constructFilter = <T extends UserAgentData | NestedValueOf<UserAgentData>>
108
106
  try {
109
107
  const value = accessor(parentObject);
110
108
  return childFilters.every((childFilter) => childFilter(value as T));
111
- } catch (error) {
109
+ } catch {
112
110
  // This happens when a user-agent lacks a nested property.
113
111
  return false;
114
112
  }
@@ -146,9 +144,34 @@ const setCumulativeWeightIndexPairs = (
146
144
  });
147
145
  };
148
146
 
149
- export class UserAgent extends Function {
147
+ // WeakSet tracking all UserAgent instances for `instanceof` support through proxies.
148
+ const userAgentInstances = new WeakSet<object>();
149
+
150
+ export class UserAgent {
151
+ static [Symbol.hasInstance](instance: unknown): boolean {
152
+ return instance instanceof Object && userAgentInstances.has(instance as object);
153
+ }
154
+
155
+ static random = (filters: Filter) => {
156
+ try {
157
+ return new UserAgent(filters);
158
+ } catch {
159
+ return null;
160
+ }
161
+ };
162
+
163
+ // Static version that creates a temporary instance with the given filters and returns the top entries.
164
+ static top = (count?: number, filters?: Filter): UserAgentData[] => {
165
+ try {
166
+ return new UserAgent(filters).top(count);
167
+ } catch {
168
+ return [];
169
+ }
170
+ };
171
+
172
+ readonly data!: UserAgentData;
173
+
150
174
  constructor(filters?: Filter) {
151
- super();
152
175
  setCumulativeWeightIndexPairs(this, constructCumulativeWeightIndexPairsFromFilters(filters));
153
176
  if (this.cumulativeWeightIndexPairs.length === 0) {
154
177
  throw new Error('No user agents matched your filters.');
@@ -156,39 +179,41 @@ export class UserAgent extends Function {
156
179
 
157
180
  this.randomize();
158
181
 
159
- // eslint-disable-next-line no-constructor-return
160
- return new Proxy(this, {
182
+ // Use a plain function as the proxy target so the `apply` trap works without
183
+ // extending `Function`, which requires `eval` and violates CSP in browser extensions.
184
+ // All property access is forwarded to the real UserAgent instance via the traps.
185
+ const target = () => {};
186
+ const proxy = new Proxy(target as unknown as this, {
161
187
  apply: () => this.random(),
162
- get: (target, property, receiver) => {
163
- const dataCandidate =
164
- target.data &&
188
+ get: (_target, property) => {
189
+ if (
190
+ this.data &&
165
191
  typeof property === 'string' &&
166
- Object.prototype.hasOwnProperty.call(target.data, property) &&
167
- Object.prototype.propertyIsEnumerable.call(target.data, property);
168
- if (dataCandidate) {
169
- const value = target.data[property as keyof UserAgentData];
192
+ Object.prototype.hasOwnProperty.call(this.data, property) &&
193
+ Object.prototype.propertyIsEnumerable.call(this.data, property)
194
+ ) {
195
+ const value = this.data[property as keyof UserAgentData];
170
196
  if (value !== undefined) {
171
197
  return value;
172
198
  }
173
199
  }
174
200
 
175
- return Reflect.get(target, property, receiver);
201
+ return Reflect.get(this, property);
176
202
  },
203
+ set: (_target, property, value) => Reflect.set(this, property, value),
204
+ defineProperty: (_target, property, descriptor) =>
205
+ Reflect.defineProperty(this, property, descriptor),
206
+ getOwnPropertyDescriptor: (_target, property) =>
207
+ Reflect.getOwnPropertyDescriptor(this, property),
208
+ has: (_target, property) => Reflect.has(this, property),
209
+ deleteProperty: (_target, property) => Reflect.deleteProperty(this, property),
210
+ ownKeys: () => Reflect.ownKeys(this),
211
+ getPrototypeOf: () => UserAgent.prototype,
177
212
  });
213
+ userAgentInstances.add(proxy);
214
+ return proxy;
178
215
  }
179
216
 
180
- static random = (filters: Filter) => {
181
- try {
182
- return new UserAgent(filters);
183
- } catch (error) {
184
- return null;
185
- }
186
- };
187
-
188
- //
189
- // Standard Object Methods
190
- //
191
-
192
217
  [Symbol.toPrimitive] = (): string => this.data.userAgent;
193
218
 
194
219
  toString = (): string => this.data.userAgent;
@@ -200,6 +225,18 @@ export class UserAgent extends Function {
200
225
  return userAgent;
201
226
  };
202
227
 
228
+ top = (count?: number): UserAgentData[] => {
229
+ // Recover individual weights from the cumulative distribution and sort by descending weight.
230
+ const pairs = this.cumulativeWeightIndexPairs;
231
+ const entries = pairs.map(([cumWeight, index], i) => ({
232
+ weight: i > 0 ? cumWeight - pairs[i - 1][0] : cumWeight,
233
+ index,
234
+ }));
235
+ entries.sort((a, b) => b.weight - a.weight);
236
+ const n = count != null ? Math.min(count, entries.length) : entries.length;
237
+ return entries.slice(0, n).map(({ index }) => structuredClone(userAgents[index]));
238
+ };
239
+
203
240
  randomize = (): void => {
204
241
  // Find a random raw random user agent.
205
242
  const randomNumber = Math.random();
@@ -210,8 +247,7 @@ export class UserAgent extends Function {
210
247
  if (index == null) {
211
248
  throw new Error('Error finding a random user agent.');
212
249
  }
213
- const rawUserAgent = userAgents[index];
214
250
 
215
- (this as { data: UserAgentData }).data = cloneDeep(rawUserAgent);
251
+ (this as { data: UserAgentData }).data = structuredClone(userAgents[index]);
216
252
  };
217
253
  }