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:
commit
4cdac37e03
@ -1 +1 @@
|
||||
repo_token: Ba2bS7pOlSLZWuESBnff8qxDjIS8Mg1Z0
|
||||
|
||||
|
||||
129
.github/copilot-instructions.md
vendored
Normal file
129
.github/copilot-instructions.md
vendored
Normal 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
19
.github/release-drafter.yml
vendored
Normal 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
|
||||
20
.github/workflows/draft-or-update-next-release.yml
vendored
Normal file
20
.github/workflows/draft-or-update-next-release.yml
vendored
Normal 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 }}
|
||||
7
.github/workflows/merge-to-master.yml
vendored
7
.github/workflows/merge-to-master.yml
vendored
@ -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
63
.github/workflows/publish.yml
vendored
Normal 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 }}
|
||||
16
.github/workflows/pull-request.yml
vendored
16
.github/workflows/pull-request.yml
vendored
@ -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
|
||||
|
||||
@ -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
9
index.d.ts
vendored
@ -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
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
{
|
||||
"name": "bowser",
|
||||
"version": "2.11.0",
|
||||
"description": "Lightweight browser detector",
|
||||
"keywords": [
|
||||
"browser",
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -126,6 +126,7 @@ export const OS_MAP = {
|
||||
WebOS: 'WebOS',
|
||||
Windows: 'Windows',
|
||||
WindowsPhone: 'Windows Phone',
|
||||
HarmonyOS: 'HarmonyOS',
|
||||
};
|
||||
|
||||
export const ENGINE_MAP = {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user