diff --git a/.coveralls.yml b/.coveralls.yml index 6d1e992..8b13789 100644 --- a/.coveralls.yml +++ b/.coveralls.yml @@ -1 +1 @@ -repo_token: Ba2bS7pOlSLZWuESBnff8qxDjIS8Mg1Z0 + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..7492a7c --- /dev/null +++ b/.github/copilot-instructions.md @@ -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. diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..131e1ca --- /dev/null +++ b/.github/release-drafter.yml @@ -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 diff --git a/.github/workflows/draft-or-update-next-release.yml b/.github/workflows/draft-or-update-next-release.yml new file mode 100644 index 0000000..a5f6567 --- /dev/null +++ b/.github/workflows/draft-or-update-next-release.yml @@ -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 }} diff --git a/.github/workflows/merge-to-master.yml b/.github/workflows/merge-to-master.yml index b22b0a0..f83149b 100644 --- a/.github/workflows/merge-to-master.yml +++ b/.github/workflows/merge-to-master.yml @@ -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 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..1e16464 --- /dev/null +++ b/.github/workflows/publish.yml @@ -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 }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index d326d04..f145bc1 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -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 diff --git a/README.md b/README.md index 242d63b..2b5c0c4 100644 --- a/README.md +++ b/README.md @@ -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`._ diff --git a/index.d.ts b/index.d.ts index d5b72c4..b78ae12 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,9 +2,16 @@ // Project: https://github.com/lancedikson/bowser // Definitions by: Alexander P. Cerutti , -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; +export const ENGINE_MAP: Record; +export const OS_MAP: Record; +export const PLATFORMS_MAP: Record; + declare namespace Bowser { /** * Creates a Parser instance diff --git a/package.json b/package.json index c719065..8411f81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "name": "bowser", - "version": "2.11.0", "description": "Lightweight browser detector", "keywords": [ "browser", diff --git a/src/bowser.js b/src/bowser.js index f79e6e0..311ce89 100644 --- a/src/bowser.js +++ b/src/bowser.js @@ -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, +}; diff --git a/src/constants.js b/src/constants.js index 98a5760..0700c3d 100644 --- a/src/constants.js +++ b/src/constants.js @@ -126,6 +126,7 @@ export const OS_MAP = { WebOS: 'WebOS', Windows: 'Windows', WindowsPhone: 'Windows Phone', + HarmonyOS: 'HarmonyOS', }; export const ENGINE_MAP = { diff --git a/src/parser-os.js b/src/parser-os.js index 4c516dd..abc2aac 100644 --- a/src/parser-os.js +++ b/src/parser-os.js @@ -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) { diff --git a/test/acceptance/useragentstrings.yml b/test/acceptance/useragentstrings.yml index c1e647a..008dc94 100644 --- a/test/acceptance/useragentstrings.yml +++ b/test/acceptance/useragentstrings.yml @@ -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" diff --git a/test/unit/bowser.js b/test/unit/bowser.js index a27b2c9..f568893 100644 --- a/test/unit/bowser.js +++ b/test/unit/bowser.js @@ -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); +});