1
0
mirror of https://github.com/lancedikson/bowser synced 2025-12-04 21:52:14 +00:00

Merge remote-tracking branch 'origin/master' into add-bot-platforms

# Conflicts:
#	src/constants.js
This commit is contained in:
naorpeled 2025-11-22 16:27:50 +02:00
commit 4cdac37e03
15 changed files with 348 additions and 18 deletions

View File

@ -1 +1 @@
repo_token: Ba2bS7pOlSLZWuESBnff8qxDjIS8Mg1Z0

129
.github/copilot-instructions.md vendored Normal file
View File

@ -0,0 +1,129 @@
# Copilot Instructions for Bowser
## Project Overview
Bowser is a small, fast, and rich-API browser/platform/engine detector for both browser and Node.js environments. It's designed to parse User-Agent strings and provide detailed information about browsers, operating systems, platforms, and rendering engines.
## Architecture
### Core Components
- **`src/bowser.js`**: Main entry point and public API. Provides static methods `getParser()` and `parse()`.
- **`src/parser.js`**: Core parsing engine that orchestrates all parsers and returns structured results.
- **`src/parser-browsers.js`**: Browser detection logic using regex patterns.
- **`src/parser-os.js`**: Operating system detection logic.
- **`src/parser-platforms.js`**: Platform type detection (desktop, tablet, mobile).
- **`src/parser-engines.js`**: Rendering engine detection (WebKit, Blink, Gecko, etc.).
- **`src/constants.js`**: Centralized constants including browser aliases and mappings.
- **`src/utils.js`**: Utility functions for string matching and manipulation.
### Build Output
- **`es5.js`**: ES5 transpiled version (default export).
- **`bundled.js`**: ES5 version with babel-polyfill included.
## Development Workflow
### Setup
```bash
npm install
```
### Key Commands
- **Build**: `npm run build` - Compiles source files using Webpack and Babel.
- **Test**: `npm test` - Runs unit and acceptance tests using AVA.
- **Lint**: `npm run lint:check` - Checks code style using ESLint.
- **Lint Fix**: `npm run lint:fix` - Auto-fixes linting issues.
- **Watch Mode**: `npm run watch` - Builds on file changes.
- **Test Watch**: `npm run test:watch` - Runs tests on file changes.
### Testing
- Tests are located in `test/acceptance/` and `test/unit/`.
- Acceptance tests use real User-Agent strings from `test/acceptance/useragentstrings.yml`.
- Always update `useragentstrings.yml` when adding browser support.
- Test framework: AVA with Babel integration.
## Coding Standards
### Style Guide
- **ESLint Config**: Based on Airbnb Base style guide.
- **Parser**: Uses `babel-eslint`.
- **Exceptions**:
- Underscore-dangle allowed for private properties.
- `no-void` disabled.
- ES6 imports must include `.js` extension.
### Naming Conventions
- **Browser Aliases**: Use lowercase letters, replace spaces/dashes with underscores, drop "browser" suffix.
- Examples: `Opera Coast``opera_coast`, `UC Browser``uc`, `SeaMonkey``seamonkey`.
- **Private Properties**: Prefix with underscore (e.g., `_ua`).
- **Constants**: Use `UPPER_SNAKE_CASE` for constant maps and aliases.
### Code Patterns
- Use ES6 modules with explicit `.js` extensions.
- Prefer static methods in Bowser class.
- Use class-based structure for Parser.
- Regex patterns should be well-documented and tested.
- Keep parsers modular and focused on single responsibility.
## Adding Browser Support
When adding support for a new browser:
1. Add regex pattern to `src/parser-browsers.js`.
2. Add browser name to `BROWSER_ALIASES_MAP` in `src/constants.js`.
3. Add corresponding entry to `BROWSER_MAP`.
4. Add test cases to `test/acceptance/useragentstrings.yml`.
5. Run tests to verify: `npm test`.
6. Check for duplicates before adding aliases.
## Branching Strategy
- **`master`**: Development branch.
- **`production`**: Production branch.
- **New Features**: Branch from `master`, PR back to `master`.
- **Hot-fixes/Browser Support**: Branch from `production`, PR back to `production`.
## Important Files
- **`index.d.ts`**: TypeScript definitions.
- **`.babelrc`**: Babel configuration for ES5 transpilation.
- **`webpack.config.js`**: Build configuration.
- **`.eslintrc.yml`**: Linting rules.
- **`package.json`**: Dependencies and scripts.
## API Design Principles
- Keep the API simple and intuitive.
- Bowser class should be stateless and provide factory methods.
- Parser class handles instance-specific logic.
- Results should be structured and predictable.
- Support both immediate parsing and lazy parsing.
## Performance Considerations
- Parsers use lazy evaluation where possible.
- Regex patterns are optimized for common browsers first.
- Optional `skipParsing` parameter for delayed parsing.
- Minimal bundle size is a priority (~4.8kB gzipped).
## Documentation
- Use JSDoc comments for all public APIs.
- Document parameters, return types, and provide examples.
- Update README.md for API changes.
- Generate docs with: `npm run generate-docs`.
## Common Pitfalls
- Always check `BROWSER_ALIASES_MAP` for existing aliases before adding new ones.
- User-Agent strings can be complex; test edge cases thoroughly.
- Remember to update both the alias map and the reverse map in constants.
- Browser versions should be treated as strings, not numbers.
- Keep regex patterns readable with comments explaining their purpose.

19
.github/release-drafter.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name-template: 'v$RESOLVED_VERSION 🌈'
tag-template: 'v$RESOLVED_VERSION'
version-resolver:
major:
labels:
- major
minor:
labels:
- minor
patch:
labels:
- patch
default: patch
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
change-title-escapes: '\<*_&'
template: |
## Changes
$CHANGES

View File

@ -0,0 +1,20 @@
name: 📝 Draft or update next release
concurrency: draft_or_update_next_release
on:
push:
branches:
- master
workflow_dispatch:
jobs:
prepare-deployment:
name: 📝 Draft or update next release
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,4 +1,4 @@
name: 'Merge to master'
name: "Merge to master"
on:
push:
@ -23,11 +23,12 @@ jobs:
shell: bash
run: echo "BRANCH_NAME=$(echo ${GITHUB_HEAD_REF} | tr / -)" >> $GITHUB_ENV
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm i -g nyc@15
- run: npm ci
- run: npm run build
- run: nyc npm test && nyc report --reporter=text-lcov | ./node_modules/coveralls/bin/coveralls.js

63
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,63 @@
name: Release
on:
# This job runs when a new release is published
release:
types: [published]
# Manual trigger with version input
workflow_dispatch:
inputs:
version:
description: "Version to release (e.g., 2.12.1)"
required: true
type: string
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 12.16.3
registry-url: "https://registry.npmjs.org"
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
# Store the release version (from release tag or manual input)
- name: Set release version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
echo "Manual release triggered for version: ${{ github.event.inputs.version }}"
else
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
echo "Release triggered from tag: ${GITHUB_REF#refs/*/}"
fi
- run: npm ci
- run: npm version $RELEASE_VERSION --no-git-tag-version
- run: npm run build
- name: Publish to npm with retry
run: |
max_attempts=5
attempt=1
while [ $attempt -le $max_attempts ]; do
echo "Attempt $attempt of $max_attempts..."
if npm publish --access public; then
echo "Successfully published!"
break
else
if [ $attempt -eq $max_attempts ]; then
echo "Failed to publish after $max_attempts attempts"
exit 1
fi
echo "Publish failed, waiting before retry..."
sleep_time=$((attempt * 30))
echo "Waiting ${sleep_time} seconds before retry..."
sleep $sleep_time
attempt=$((attempt + 1))
fi
done
env:
NODE_AUTH_TOKEN: ${{ secrets.BOWSER_NPM_PUBLISH_TOKEN }}

View File

@ -1,4 +1,4 @@
name: 'Pull Request'
name: "Pull Request"
on:
pull_request:
types: [opened, reopened, synchronize]
@ -11,12 +11,12 @@ jobs:
node: [12.16.3]
name: Node ${{ matrix.node }}
steps:
- name: 'Checkout latest code'
uses: actions/checkout@v3
- name: "Checkout latest code"
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- name: Install dependencies
@ -27,17 +27,17 @@ jobs:
run: npm run test
lint:
name: 'ESLint'
name: "ESLint"
runs-on: ubuntu-latest
steps:
- name: Checkout latest code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '16'
node-version: "16"
- name: Install dependencies
run: npm ci
- name: Run ESLint

View File

@ -11,8 +11,6 @@ Don't hesitate to support the project on Github or [OpenCollective](https://open
# Contents
- [Overview](#overview)
- [Use cases](#use-cases)
- [Advanced usage](#advanced-usage)
- [How can I help?](#contributing)
# Overview
@ -20,7 +18,7 @@ The library is made to help to detect what browser your user has and gives you a
### ⚠️ Version 2.0 breaking changes ⚠️
Version 2.0 has drastically changed the API. All available methods are on the [docs page](https://lancedikson.github.io/bowser/docs).
Version 2.0 has drastically changed the API. All available methods are on the [docs page](https://bowser-js.github.io/bowser/docs/).
_For legacy code, check out the [1.x](https://github.com/lancedikson/bowser/tree/v1.x) branch and install it through `npm install bowser@1.9.4`._

9
index.d.ts vendored
View File

@ -2,9 +2,16 @@
// Project: https://github.com/lancedikson/bowser
// Definitions by: Alexander P. Cerutti <https://github.com/alexandercerutti>,
export default Bowser;
export = Bowser;
export as namespace Bowser;
export function getParser(UA: string, skipParsing?: boolean): Bowser.Parser.Parser;
export function parse(UA: string): Bowser.Parser.ParsedResult;
export const BROWSER_MAP: Record<string, string>;
export const ENGINE_MAP: Record<string, string>;
export const OS_MAP: Record<string, string>;
export const PLATFORMS_MAP: Record<string, string>;
declare namespace Bowser {
/**
* Creates a Parser instance

View File

@ -1,6 +1,5 @@
{
"name": "bowser",
"version": "2.11.0",
"description": "Lightweight browser detector",
"keywords": [
"browser",

View File

@ -75,3 +75,9 @@ class Bowser {
}
export default Bowser;
export const { getParser } = Bowser;
export const { parse } = Bowser;
export {
BROWSER_MAP, ENGINE_MAP, OS_MAP, PLATFORMS_MAP,
};

View File

@ -126,6 +126,7 @@ export const OS_MAP = {
WebOS: 'WebOS',
Windows: 'Windows',
WindowsPhone: 'Windows Phone',
HarmonyOS: 'HarmonyOS',
};
export const ENGINE_MAP = {

View File

@ -87,6 +87,18 @@ export default [
},
},
/* HarmonyOS */
{
test: [/OpenHarmony/i],
describe(ua) {
const version = Utils.getFirstMatch(/OpenHarmony\s+(\d+(\.\d+)*)/i, ua);
return {
name: OS_MAP.HarmonyOS,
version,
};
},
},
/* Android */
{
test(parser) {

View File

@ -365,6 +365,20 @@
type: "mobile"
engine:
name: "Blink"
-
ua: "Mozilla/5.0 (Phone; OpenHarmony 5.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 ArkWeb/4.1.6.1 Mobile HuaweiBrowser/5.1.5.352"
spec:
browser:
name: "Chrome"
version: "114.0.0.0"
os:
name: "HarmonyOS"
version: "5.0"
platform:
type: "mobile"
vendor: "Huawei"
engine:
name: "Blink"
Google Search:
-
ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 12_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/83.0.268992909 Mobile/15E148 Safari/605.1"

View File

@ -1,5 +1,5 @@
import test from 'ava';
import Bowser from '../../src/bowser';
import Bowser, { getParser, parse, BROWSER_MAP, ENGINE_MAP, OS_MAP, PLATFORMS_MAP } from '../../src/bowser';
import Parser from '../../src/parser';
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 OPR/43.0.2442.1165';
@ -16,3 +16,64 @@ test('Bowser`s constructor fails if UA is empty', (t) => {
test('Bowser.parse parses UA and returns result', (t) => {
t.deepEqual(Bowser.parse(UA), browser.getResult());
});
test('Named export getParser works like Bowser.getParser', (t) => {
const namedExportParser = getParser(UA);
t.truthy(namedExportParser instanceof Parser);
t.deepEqual(namedExportParser.getResult(), browser.getResult());
});
test('Named export getParser with skipParsing parameter', (t) => {
const parserWithSkip = getParser(UA, true);
t.truthy(parserWithSkip instanceof Parser);
// With skipParsing=true, the result should be undefined until we explicitly parse
t.deepEqual(parserWithSkip.getResult(), Bowser.getParser(UA, true).getResult());
});
test('Named export getParser throws error for invalid UA', (t) => {
t.throws(() => getParser(undefined), { message: 'UserAgent should be a string' });
t.throws(() => getParser(123), { message: 'UserAgent should be a string' });
t.throws(() => getParser(null), { message: 'UserAgent should be a string' });
t.throws(() => getParser({}), { message: 'UserAgent should be a string' });
});
test('Named export parse works like Bowser.parse', (t) => {
t.deepEqual(parse(UA), Bowser.parse(UA));
});
test('Named export parse produces consistent results', (t) => {
const result1 = parse(UA);
const result2 = parse(UA);
t.deepEqual(result1, result2);
t.deepEqual(result1, Bowser.parse(UA));
});
test('Named exports of constants are available', (t) => {
t.truthy(BROWSER_MAP);
t.truthy(ENGINE_MAP);
t.truthy(OS_MAP);
t.truthy(PLATFORMS_MAP);
t.is(BROWSER_MAP, Bowser.BROWSER_MAP);
t.is(ENGINE_MAP, Bowser.ENGINE_MAP);
t.is(OS_MAP, Bowser.OS_MAP);
t.is(PLATFORMS_MAP, Bowser.PLATFORMS_MAP);
});
test('Named exports constants are objects with expected structure', (t) => {
t.is(typeof BROWSER_MAP, 'object');
t.is(typeof ENGINE_MAP, 'object');
t.is(typeof OS_MAP, 'object');
t.is(typeof PLATFORMS_MAP, 'object');
});
test('All named exports work together', (t) => {
// Test that we can use multiple named exports in the same scope
const result = parse(UA);
const parser = getParser(UA);
t.deepEqual(result, parser.getResult());
t.truthy(BROWSER_MAP);
t.truthy(ENGINE_MAP);
t.truthy(OS_MAP);
t.truthy(PLATFORMS_MAP);
});