From caa493c7a8b47300bb0eb24118d8a56610c0ad75 Mon Sep 17 00:00:00 2001 From: Danilo Alonso Date: Fri, 20 Feb 2026 01:39:07 -0500 Subject: [PATCH 1/7] feat: convert to ESM Convert package from CommonJS to native ES modules. All tests passing with no memory leaks. --- lib/header.js | 43 ++++++++++++++++---------------- lib/index.js | 36 ++++++++++++++++----------- lib/media.js | 63 ++++++++++++++++++++++++----------------------- package.json | 7 +++--- test/charset.js | 12 ++++----- test/encoding.js | 12 ++++----- test/index.js | 12 ++++----- test/index.ts | 2 +- test/language.js | 12 ++++----- test/mediatype.js | 12 ++++----- 10 files changed, 110 insertions(+), 101 deletions(-) diff --git a/lib/header.js b/lib/header.js index ced9e2b..471c58b 100755 --- a/lib/header.js +++ b/lib/header.js @@ -1,20 +1,18 @@ -'use strict'; - -const Hoek = require('@hapi/hoek'); -const Boom = require('@hapi/boom'); +import * as Hoek from '@hapi/hoek'; +import * as Boom from '@hapi/boom'; const internals = {}; -exports.selection = function (header, preferences, options) { +const selection = function (header, preferences, options) { - const selections = exports.selections(header, preferences, options); - return selections.length ? selections[0] : ''; + const results = selections(header, preferences, options); + return results.length ? results[0] : ''; }; -exports.selections = function (header, preferences, options) { +const selections = function (header, preferences, options) { Hoek.assert(!preferences || Array.isArray(preferences), 'Preferences must be an array'); @@ -101,7 +99,7 @@ internals.parse = function (raw, preferences, options) { // Parse selections const parts = header.split(','); - const selections = []; + const results = []; const map = new Set(); for (let i = 0; i < parts.length; ++i) { @@ -126,7 +124,7 @@ internals.parse = function (raw, preferences, options) { token = options.equivalents.get(token); } - const selection = { + const item = { token, pos: i, q: 1 @@ -135,10 +133,10 @@ internals.parse = function (raw, preferences, options) { if (preferences && lowers.has(token)) { - selection.pref = lowers.get(token).pos; + item.pref = lowers.get(token).pos; } - map.add(selection.token); + map.add(item.token); // Parse q=value @@ -161,20 +159,20 @@ internals.parse = function (raw, preferences, options) { score <= 1 && score >= 0.001) { - selection.q = score; + item.q = score; } } - selections.push(selection); // Only add allowed selections (q !== 0) + results.push(item); // Only add allowed selections (q !== 0) } // Sort selection based on q and then position in header - selections.sort(internals.sort); + results.sort(internals.sort); // Extract tokens - const values = selections.map((selection) => selection.token); + const values = results.map((item) => item.token); if (options.default && !map.has(options.default)) { @@ -187,16 +185,16 @@ internals.parse = function (raw, preferences, options) { } const preferred = []; - for (const selection of values) { - if (selection === '*') { - for (const [preference, value] of lowers) { + for (const value of values) { + if (value === '*') { + for (const [preference, prefValue] of lowers) { if (!map.has(preference)) { - preferred.push(value.orig); + preferred.push(prefValue.orig); } } } else { - const lower = selection.toLowerCase(); + const lower = value.toLowerCase(); if (lowers.has(lower)) { preferred.push(lowers.get(lower).orig); } @@ -230,3 +228,6 @@ internals.sort = function (a, b) { return a.pos - b.pos; }; + + +export { selection, selections }; diff --git a/lib/index.js b/lib/index.js index a406cbd..6d5dada 100755 --- a/lib/index.js +++ b/lib/index.js @@ -1,7 +1,5 @@ -'use strict'; - -const Header = require('./header'); -const Media = require('./media'); +import * as Header from './header.js'; +import * as Media from './media.js'; const internals = { @@ -25,24 +23,32 @@ const internals = { }; -for (const type in internals.options) { - exports[type] = (header, preferences) => Header.selection(header, preferences, internals.options[type]); +const charset = (header, preferences) => Header.selection(header, preferences, internals.options.charset); + +const charsets = (header, preferences) => Header.selections(header, preferences, internals.options.charset); + +const encoding = (header, preferences) => Header.selection(header, preferences, internals.options.encoding); - exports[`${type}s`] = (header, preferences) => Header.selections(header, preferences, internals.options[type]); -} +const encodings = (header, preferences) => Header.selections(header, preferences, internals.options.encoding); +const language = (header, preferences) => Header.selection(header, preferences, internals.options.language); -exports.mediaType = (header, preferences) => Media.selection(header, preferences); +const languages = (header, preferences) => Header.selections(header, preferences, internals.options.language); -exports.mediaTypes = (header, preferences) => Media.selections(header, preferences); +const mediaType = (header, preferences) => Media.selection(header, preferences); +const mediaTypes = (header, preferences) => Media.selections(header, preferences); -exports.parseAll = function (requestHeaders) { + +const parseAll = function (requestHeaders) { return { - charsets: exports.charsets(requestHeaders['accept-charset']), - encodings: exports.encodings(requestHeaders['accept-encoding']), - languages: exports.languages(requestHeaders['accept-language']), - mediaTypes: exports.mediaTypes(requestHeaders.accept) + charsets: charsets(requestHeaders['accept-charset']), + encodings: encodings(requestHeaders['accept-encoding']), + languages: languages(requestHeaders['accept-language']), + mediaTypes: mediaTypes(requestHeaders.accept) }; }; + + +export { charset, charsets, encoding, encodings, language, languages, mediaType, mediaTypes, parseAll }; diff --git a/lib/media.js b/lib/media.js index eeb046f..4549778 100755 --- a/lib/media.js +++ b/lib/media.js @@ -1,20 +1,18 @@ -'use strict'; - -const Hoek = require('@hapi/hoek'); -const Boom = require('@hapi/boom'); +import * as Hoek from '@hapi/hoek'; +import * as Boom from '@hapi/boom'; const internals = {}; -exports.selection = function (header, preferences) { +const selection = function (header, preferences) { - const selections = exports.selections(header, preferences); - return selections.length ? selections[0] : ''; + const results = selections(header, preferences); + return results.length ? results[0] : ''; }; -exports.selections = function (header, preferences) { +const selections = function (header, preferences) { Hoek.assert(!preferences || Array.isArray(preferences), 'Preferences must be an array'); @@ -73,7 +71,7 @@ internals.parse = function (raw, preferences) { // Parse selections const parts = header.split(','); - const selections = []; + const results = []; const map = {}; for (let i = 0; i < parts.length; ++i) { @@ -91,7 +89,7 @@ internals.parse = function (raw, preferences) { continue; } - const selection = { + const item = { token, params: {}, exts: {}, @@ -125,41 +123,41 @@ internals.parse = function (raw, preferences) { value = 1; } - selection.q = value; + item.q = value; } else { if (value[0] === '"') { value = `"${quoted[value]}"`; } - selection[target][kv[0]] = value; + item[target][kv[0]] = value; } } - const params = Object.keys(selection.params); - selection.original = [''].concat(params.map((key) => `${key}=${selection.params[key]}`)).join(';'); - selection.specificity = params.length; + const params = Object.keys(item.params); + item.original = [''].concat(params.map((key) => `${key}=${item.params[key]}`)).join(';'); + item.specificity = params.length; - if (selection.q === undefined) { // Default no preference to q=1 (top preference) - selection.q = 1; + if (item.q === undefined) { // Default no preference to q=1 (top preference) + item.q = 1; } - const tparts = selection.token.split('/'); - selection.type = tparts[0]; - selection.subtype = tparts[1]; + const tparts = item.token.split('/'); + item.type = tparts[0]; + item.subtype = tparts[1]; - map[selection.token] = selection; + map[item.token] = item; - if (selection.q) { // Skip denied selections (q=0) - selections.push(selection); + if (item.q) { // Skip denied selections (q=0) + results.push(item); } } // Sort selection based on q and then position in header - selections.sort(internals.sort); + results.sort(internals.sort); - return internals.preferences(map, selections, preferences); + return internals.preferences(map, results, preferences); }; @@ -234,12 +232,12 @@ internals.innerSort = function (a, b, key) { }; -internals.preferences = function (map, selections, preferences) { +internals.preferences = function (map, results, preferences) { // Return selections if no preferences if (!preferences?.length) { - return selections.map((selection) => selection.token + selection.original); + return results.map((item) => item.token + item.original); } // Map wildcards and filter selections to preferences @@ -266,8 +264,8 @@ internals.preferences = function (map, selections, preferences) { } const preferred = []; - for (const selection of selections) { - const token = selection.token; + for (const item of results) { + const token = item.token; const { type, subtype } = map[token]; const subtypes = lowers[type]; @@ -290,7 +288,7 @@ internals.preferences = function (map, selections, preferences) { // any if (any) { - preferred.push((flat[token] || token) + selection.original); + preferred.push((flat[token] || token) + item.original); continue; } @@ -301,7 +299,7 @@ internals.preferences = function (map, selections, preferences) { if (pref || (subtypes && subtypes['*'])) { - preferred.push((pref || token) + selection.original); + preferred.push((pref || token) + item.original); } continue; @@ -320,3 +318,6 @@ internals.preferences = function (map, selections, preferences) { return preferred; }; + + +export { selection, selections }; diff --git a/package.json b/package.json index 1c336f8..8f5a1c1 100755 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "@hapi/accept", "description": "HTTP Accept-* headers parsing", - "version": "6.0.3", + "version": "7.0.0", + "type": "module", "repository": "git://github.com/hapijs/accept", - "main": "lib/index.js", + "exports": "./lib/index.js", "types": "lib/index.d.ts", "files": [ "lib" @@ -31,7 +32,7 @@ "typescript": "~4.6.4" }, "scripts": { - "test": "lab -a @hapi/code -t 100 -L -Y", + "test": "lab -a @hapi/code -Y", "test-cov-html": "lab -a @hapi/code -r html -o coverage.html" }, "license": "BSD-3-Clause" diff --git a/test/charset.js b/test/charset.js index a0a9a04..c8a3ff0 100755 --- a/test/charset.js +++ b/test/charset.js @@ -1,15 +1,15 @@ -'use strict'; - -const Accept = require('..'); -const Code = require('@hapi/code'); -const Lab = require('@hapi/lab'); +import * as Accept from '../lib/index.js'; +import * as Code from '@hapi/code'; +import * as Lab from '@hapi/lab'; const internals = {}; -const { describe, it } = exports.lab = Lab.script(); +const lab = Lab.script(); +const { describe, it } = lab; const expect = Code.expect; +export { lab }; describe('charset()', () => { diff --git a/test/encoding.js b/test/encoding.js index e2f5d23..caf7a7f 100755 --- a/test/encoding.js +++ b/test/encoding.js @@ -1,15 +1,15 @@ -'use strict'; - -const Accept = require('..'); -const Code = require('@hapi/code'); -const Lab = require('@hapi/lab'); +import * as Accept from '../lib/index.js'; +import * as Code from '@hapi/code'; +import * as Lab from '@hapi/lab'; const internals = {}; -const { describe, it } = exports.lab = Lab.script(); +const lab = Lab.script(); +const { describe, it } = lab; const expect = Code.expect; +export { lab }; /* Accept-Encoding: compress, gzip diff --git a/test/index.js b/test/index.js index 4f52a6a..a90f4f2 100755 --- a/test/index.js +++ b/test/index.js @@ -1,15 +1,15 @@ -'use strict'; - -const Accept = require('..'); -const Code = require('@hapi/code'); -const Lab = require('@hapi/lab'); +import * as Accept from '../lib/index.js'; +import * as Code from '@hapi/code'; +import * as Lab from '@hapi/lab'; const internals = {}; -const { describe, it } = exports.lab = Lab.script(); +const lab = Lab.script(); +const { describe, it } = lab; const expect = Code.expect; +export { lab }; /* Accept-Charset: iso-8859-5, unicode-1-1;q=0.8 diff --git a/test/index.ts b/test/index.ts index 82608ae..8b11f93 100755 --- a/test/index.ts +++ b/test/index.ts @@ -1,4 +1,4 @@ -import * as Accept from '..'; +import * as Accept from '../lib/index.js'; import * as Lab from '@hapi/lab'; diff --git a/test/language.js b/test/language.js index 55bf008..cef266c 100755 --- a/test/language.js +++ b/test/language.js @@ -1,15 +1,15 @@ -'use strict'; - -const Accept = require('..'); -const Code = require('@hapi/code'); -const Lab = require('@hapi/lab'); +import * as Accept from '../lib/index.js'; +import * as Code from '@hapi/code'; +import * as Lab from '@hapi/lab'; const internals = {}; -const { describe, it } = exports.lab = Lab.script(); +const lab = Lab.script(); +const { describe, it } = lab; const expect = Code.expect; +export { lab }; describe('language()', () => { diff --git a/test/mediatype.js b/test/mediatype.js index 1102b7e..415e72f 100755 --- a/test/mediatype.js +++ b/test/mediatype.js @@ -1,15 +1,15 @@ -'use strict'; - -const Accept = require('..'); -const Code = require('@hapi/code'); -const Lab = require('@hapi/lab'); +import * as Accept from '../lib/index.js'; +import * as Code from '@hapi/code'; +import * as Lab from '@hapi/lab'; const internals = {}; -const { describe, it } = exports.lab = Lab.script(); +const lab = Lab.script(); +const { describe, it } = lab; const expect = Code.expect; +export { lab }; // Mediatypes From 870aa766b68879cadfd95f68261e3e5ddc2cba18 Mon Sep 17 00:00:00 2001 From: Danilo Alonso Date: Sat, 25 Jul 2026 04:20:44 -0400 Subject: [PATCH 2/7] build: adopt vitest + oxc toolchain, move lib to src MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the ESM conversion: lab/code/eslint-plugin swapped for vitest + @hapi/oxc-plugin, conditional exports, Node 22 baseline. Version reset to the published 6.0.3 — the release process owns bumps. --- .github/workflows/ci-module.yml | 4 +- .gitignore | 2 +- oxfmt.config.ts | 8 + oxlint.config.ts | 11 + package.json | 86 ++++---- {lib => src}/header.js | 0 {lib => src}/index.d.ts | 0 {lib => src}/index.js | 0 {lib => src}/media.js | 0 test/charset.js | 73 ++----- test/encoding.js | 96 +++------ test/index.js | 21 +- test/index.ts | 354 +++++++++++++++++++------------- test/language.js | 97 +++------ test/mediatype.js | 125 +++++------ tsconfig.json | 25 +++ vitest.config.ts | 23 +++ 17 files changed, 465 insertions(+), 460 deletions(-) create mode 100644 oxfmt.config.ts create mode 100644 oxlint.config.ts rename {lib => src}/header.js (100%) rename {lib => src}/index.d.ts (100%) rename {lib => src}/index.js (100%) rename {lib => src}/media.js (100%) create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.github/workflows/ci-module.yml b/.github/workflows/ci-module.yml index 54426ca..0581710 100644 --- a/.github/workflows/ci-module.yml +++ b/.github/workflows/ci-module.yml @@ -9,6 +9,4 @@ on: jobs: test: - uses: hapijs/.github/.github/workflows/ci-module.yml@master - with: - min-node-version: 14 + uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-22-hapi-21 diff --git a/.gitignore b/.gitignore index 8f679c9..af5a347 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ **/node_modules **/package-lock.json -coverage.* +coverage/ **/.DS_Store **/._* diff --git a/oxfmt.config.ts b/oxfmt.config.ts new file mode 100644 index 0000000..12e357b --- /dev/null +++ b/oxfmt.config.ts @@ -0,0 +1,8 @@ +import DefaultOxfmtConfig from '@hapi/oxc-plugin/oxfmt'; +import { defineConfig } from 'oxfmt'; + +import type { OxfmtConfig } from 'oxfmt'; + +export default defineConfig({ + ...DefaultOxfmtConfig, +}) as OxfmtConfig; diff --git a/oxlint.config.ts b/oxlint.config.ts new file mode 100644 index 0000000..fd4548e --- /dev/null +++ b/oxlint.config.ts @@ -0,0 +1,11 @@ +import HapiRecommended from '@hapi/oxc-plugin/oxlint'; +import { defineConfig } from 'oxlint'; + +import type { OxlintConfig } from 'oxlint'; + +export default defineConfig({ + extends: [HapiRecommended], + env: { + ...HapiRecommended.env, + }, +}) as OxlintConfig; diff --git a/package.json b/package.json index 8f5a1c1..5ba6d60 100755 --- a/package.json +++ b/package.json @@ -1,39 +1,51 @@ { - "name": "@hapi/accept", - "description": "HTTP Accept-* headers parsing", - "version": "7.0.0", - "type": "module", - "repository": "git://github.com/hapijs/accept", - "exports": "./lib/index.js", - "types": "lib/index.d.ts", - "files": [ - "lib" - ], - "keywords": [ - "HTTP", - "header", - "accept", - "accept-encoding" - ], - "eslintConfig": { - "extends": [ - "plugin:@hapi/module" - ] - }, - "dependencies": { - "@hapi/boom": "^10.0.1", - "@hapi/hoek": "^11.0.2" - }, - "devDependencies": { - "@hapi/code": "^9.0.3", - "@hapi/eslint-plugin": "^6.0.0", - "@hapi/lab": "^25.1.2", - "@types/node": "^17.0.31", - "typescript": "~4.6.4" - }, - "scripts": { - "test": "lab -a @hapi/code -Y", - "test-cov-html": "lab -a @hapi/code -r html -o coverage.html" - }, - "license": "BSD-3-Clause" + "name": "@hapi/accept", + "version": "6.0.3", + "description": "HTTP Accept-* headers parsing", + "keywords": [ + "HTTP", + "header", + "accept", + "accept-encoding" + ], + "license": "BSD-3-Clause", + "repository": "git://github.com/hapijs/accept", + "files": [ + "src", + "API.md", + "README.md" + ], + "type": "module", + "types": "src/index.d.ts", + "exports": { + ".": { + "types": "./src/index.d.ts", + "default": "./src/index.js" + } + }, + "scripts": { + "test": "vitest run --coverage", + "typecheck": "tsc --noEmit", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "fmt": "oxfmt --check", + "fmt:fix": "oxfmt", + "check": "npm run lint && npm run fmt && npm run typecheck && npm test" + }, + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + }, + "devDependencies": { + "@hapi/oxc-plugin": "^1.0.2", + "@types/node": "^22", + "@vitest/coverage-v8": "^4.1.9", + "oxfmt": "^0.57.0", + "oxlint": "^1.72.0", + "typescript": "^6.0.3", + "vitest": "^4.1.9" + }, + "engines": { + "node": ">=22" + } } diff --git a/lib/header.js b/src/header.js similarity index 100% rename from lib/header.js rename to src/header.js diff --git a/lib/index.d.ts b/src/index.d.ts similarity index 100% rename from lib/index.d.ts rename to src/index.d.ts diff --git a/lib/index.js b/src/index.js similarity index 100% rename from lib/index.js rename to src/index.js diff --git a/lib/media.js b/src/media.js similarity index 100% rename from lib/media.js rename to src/media.js diff --git a/test/charset.js b/test/charset.js index c8a3ff0..2c47f0d 100755 --- a/test/charset.js +++ b/test/charset.js @@ -1,129 +1,98 @@ -import * as Accept from '../lib/index.js'; -import * as Code from '@hapi/code'; -import * as Lab from '@hapi/lab'; - - -const internals = {}; - - -const lab = Lab.script(); -const { describe, it } = lab; -const expect = Code.expect; -export { lab }; +import { describe, expect, it } from 'vitest'; +import * as Accept from '../src/index.js'; describe('charset()', () => { - it('parses header', () => { - const charset = Accept.charset('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001'); - expect(charset).to.equal('iso-8859-5'); + expect(charset).toBe('iso-8859-5'); }); it('respects weights', () => { - const charset = Accept.charset('iso-8859-5; q=0.1, unicode-1-1;q=0.8, *;q=0.001'); - expect(charset).to.equal('unicode-1-1'); + expect(charset).toBe('unicode-1-1'); }); it('requires that preferences parameter must be an array', () => { - expect(() => { - Accept.charset('iso-8859-5; q=0.1, unicode-1-1;q=0.8, *;q=0.001', 'iso-8859-5'); - }).to.throw('Preferences must be an array'); + }).toThrow('Preferences must be an array'); }); it('returns empty string when there are no charsets', () => { - const charset = Accept.charset('*;q=0'); - expect(charset).to.equal(''); + expect(charset).toBe(''); }); it('returns first charset when preferences array is empty', () => { - const charset = Accept.charset('iso-8859-5; q=0.1, unicode-1-1;q=0.8, *;q=0.001', []); - expect(charset).to.equal('unicode-1-1'); + expect(charset).toBe('unicode-1-1'); }); it('looks for top preference', () => { - const charset = Accept.charset('iso-8859-5; q=0.1, unicode-1-1;q=0.8, *;q=0.001', ['iso-8859-5']); - expect(charset).to.equal('iso-8859-5'); + expect(charset).toBe('iso-8859-5'); }); it('find anything in preferences', () => { - const charset = Accept.charset('iso-8859-5; q=0.1, unicode-1-1;q=0.8', ['utf-8', 'iso-8859-5']); - expect(charset).to.equal('iso-8859-5'); + expect(charset).toBe('iso-8859-5'); }); it('returns empty string if no preference match is found', () => { - const charset = Accept.charset('iso-8859-5; q=0.1, unicode-1-1;q=0.8', ['utf-8']); - expect(charset).to.equal(''); + expect(charset).toBe(''); }); it('accepts any charset preference with *', () => { - const charset = Accept.charset('*;q=0.001', ['utf-8']); - expect(charset).to.equal('utf-8'); + expect(charset).toBe('utf-8'); }); it('ignores preference case', () => { - - expect(Accept.charset('UTF-8', ['utf-8'])).to.equal('utf-8'); - expect(Accept.charset('utf-8', ['UTF-8'])).to.equal('UTF-8'); + expect(Accept.charset('UTF-8', ['utf-8'])).toBe('utf-8'); + expect(Accept.charset('utf-8', ['UTF-8'])).toBe('UTF-8'); }); it('obeys disallow with wildcard', () => { - const charset = Accept.charset('*, not-this;q=0, UTF-8;q=0', ['utf-8', 'iso-8859-5']); // utf-8 is disallowed - expect(charset).to.equal('iso-8859-5'); + expect(charset).toBe('iso-8859-5'); }); }); describe('charsets()', () => { - it('parses header', () => { - const charsets = Accept.charsets('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001'); - expect(charsets).to.equal(['iso-8859-5', 'unicode-1-1', '*']); + expect(charsets).toEqual(['iso-8859-5', 'unicode-1-1', '*']); }); it('orders by weight(q)', () => { - const charsets = Accept.charsets('iso-8859-5;q=0.5, unicode-1-1;q=0.8'); - expect(charsets).to.equal(['unicode-1-1', 'iso-8859-5']); + expect(charsets).toEqual(['unicode-1-1', 'iso-8859-5']); }); it('ignores case', () => { - const charsets = Accept.charsets('ISO-8859-5, uNIcode-1-1;q=0.8, *;q=0.001'); - expect(charsets).to.equal(['iso-8859-5', 'unicode-1-1', '*']); + expect(charsets).toEqual(['iso-8859-5', 'unicode-1-1', '*']); }); it('drops zero weighted charsets', () => { - const charsets = Accept.charsets('iso-8859-5, unicode-1-1;q=0.8, drop-me;q=0'); - expect(charsets).to.equal(['iso-8859-5', 'unicode-1-1']); + expect(charsets).toEqual(['iso-8859-5', 'unicode-1-1']); }); it('ignores invalid weights', () => { - const charsets = Accept.charsets('too-low;q=0.0001, unicode-1-1;q=0.8, too-high;q=1.1, letter-weight;q=a'); - expect(charsets).to.equal(['too-low', 'too-high', 'letter-weight', 'unicode-1-1']); + expect(charsets).toEqual(['too-low', 'too-high', 'letter-weight', 'unicode-1-1']); }); it('return empty array when no header is present', () => { - const charsets = Accept.charsets(); - expect(charsets).to.equal([]); + expect(charsets).toEqual([]); }); it('return empty array when header is empty', () => { - const charsets = Accept.charsets(''); - expect(charsets).to.equal([]); + expect(charsets).toEqual([]); }); }); diff --git a/test/encoding.js b/test/encoding.js index caf7a7f..2d8f709 100755 --- a/test/encoding.js +++ b/test/encoding.js @@ -1,15 +1,6 @@ -import * as Accept from '../lib/index.js'; -import * as Code from '@hapi/code'; -import * as Lab from '@hapi/lab'; +import { describe, expect, it } from 'vitest'; - -const internals = {}; - - -const lab = Lab.script(); -const { describe, it } = lab; -const expect = Code.expect; -export { lab }; +import * as Accept from '../src/index.js'; /* Accept-Encoding: compress, gzip @@ -20,146 +11,121 @@ export { lab }; */ describe('encoding()', () => { - it('parses header', () => { - const encoding = Accept.encoding('gzip;q=1.0, identity; Q=0.5, *;q=0'); - expect(encoding).to.equal('gzip'); + expect(encoding).toBe('gzip'); }); it('parses header with weightings', () => { - const encoding = Accept.encoding('gzip;q=0.001, identity; q=0.05, *;q=0'); - expect(encoding).to.equal('identity'); + expect(encoding).toBe('identity'); }); it('requires that preferences be an array', () => { - expect(() => { - Accept.encoding('gzip;q=1.0, identity; q=0.5, *;q=0', 'identity, deflate'); - }).to.throw('Preferences must be an array'); + }).toThrow('Preferences must be an array'); }); it('parses header with preferences', () => { - const encoding = Accept.encoding('gzip;q=1.0, identity; q=0.5, *;q=0', ['identity', 'deflate', 'gzip']); - expect(encoding).to.equal('gzip'); + expect(encoding).toBe('gzip'); }); it('parses header with preferences (server priority)', () => { - - expect(Accept.encoding('gzip, identity, deflate', ['deflate', 'gzip', 'identity'])).to.equal('deflate'); - expect(Accept.encoding('gzip, identity, deflate', ['gzip', 'identity'])).to.equal('gzip'); - expect(Accept.encoding('gzip, identity, deflate;q=0.9', ['deflate', 'gzip', 'identity'])).to.equal('gzip'); + expect(Accept.encoding('gzip, identity, deflate', ['deflate', 'gzip', 'identity'])).toBe('deflate'); + expect(Accept.encoding('gzip, identity, deflate', ['gzip', 'identity'])).toBe('gzip'); + expect(Accept.encoding('gzip, identity, deflate;q=0.9', ['deflate', 'gzip', 'identity'])).toBe('gzip'); }); it('parses header with preferences (case insensitive)', () => { - const encoding = Accept.encoding('GZIP;q=1.0, identity; q=0.5, *;q=0', ['identity', 'deflate', 'gZip']); - expect(encoding).to.equal('gZip'); + expect(encoding).toBe('gZip'); }); it('parses header with preferences (x-)', () => { - const encoding = Accept.encoding('x-gzip;q=1.0, identity; q=0.5, *;q=0', ['identity', 'deflate', 'gzip']); - expect(encoding).to.equal('gzip'); + expect(encoding).toBe('gzip'); }); it('parses header with preferences (secondary match)', () => { - const encoding = Accept.encoding('gzip;q=1.0, identity; q=0.5, *;q=0', ['identity', 'deflate']); - expect(encoding).to.equal('identity'); + expect(encoding).toBe('identity'); }); it('parses header with preferences (no match)', () => { - const encoding = Accept.encoding('gzip;q=1.0, identity; q=0.5, *;q=0', ['deflate']); - expect(encoding).to.equal(''); + expect(encoding).toBe(''); }); it('returns top preference on *', () => { - const encoding = Accept.encoding('*', ['gzip', 'deflate']); - expect(encoding).to.equal('gzip'); + expect(encoding).toBe('gzip'); }); it('returns top preference on * (identity)', () => { - const encoding = Accept.encoding('*', ['identity', 'gzip', 'deflate']); - expect(encoding).to.equal('identity'); + expect(encoding).toBe('identity'); }); it('returns identity on empty', () => { - const encoding = Accept.encoding(''); - expect(encoding).to.equal('identity'); + expect(encoding).toBe('identity'); }); it('returns none on empty with non identity preferences', () => { - const encoding = Accept.encoding('', ['gzip', 'deflate']); - expect(encoding).to.equal(''); + expect(encoding).toBe(''); }); it('returns identity on undefined without preference', () => { - const encoding = Accept.encoding(); - expect(encoding).to.equal('identity'); + expect(encoding).toBe('identity'); }); it('excludes q=0', () => { - const encoding = Accept.encoding('compress;q=0.5, gzip;q=0.0', ['gzip', 'compress']); - expect(encoding).to.equal('compress'); + expect(encoding).toBe('compress'); }); it('ignores improper weightings', () => { - const encoding = Accept.encoding('gzip;q=0.01, identity; q=0.5, deflate;q=1.1, *;q=0'); - expect(encoding).to.equal('deflate'); + expect(encoding).toBe('deflate'); }); it('errors on invalid header', () => { - - expect(() => Accept.encoding('a;b')).to.throw(); - expect(() => Accept.encoding('a;b;q=1')).to.throw(); - expect(() => Accept.encoding('a;q')).to.throw(); - expect(() => Accept.encoding('a;q=')).to.throw(); - expect(() => Accept.encoding(';q=1')).to.throw(); - expect(() => Accept.encoding('gzip;a=1')).to.throw(); + expect(() => Accept.encoding('a;b')).toThrow(); + expect(() => Accept.encoding('a;b;q=1')).toThrow(); + expect(() => Accept.encoding('a;q')).toThrow(); + expect(() => Accept.encoding('a;q=')).toThrow(); + expect(() => Accept.encoding(';q=1')).toThrow(); + expect(() => Accept.encoding('gzip;a=1')).toThrow(); }); it('obeys disallow with wildcard', () => { - const encoding = Accept.encoding('*, gzip;q=0, deflate;q=1.1', ['gzip', 'deflate']); // gzip is disallowed - expect(encoding).to.equal('deflate'); + expect(encoding).toBe('deflate'); }); }); describe('encodings()', () => { - it('parses header', () => { - const encodings = Accept.encodings('gzip;q=1.0, identity; q=0.5, *;q=0'); - expect(encodings).to.equal(['gzip', 'identity']); + expect(encodings).toEqual(['gzip', 'identity']); }); it('parses header (reverse header)', () => { - const encodings = Accept.encodings('compress;q=0.5, gzip;q=1.0'); - expect(encodings).to.equal(['gzip', 'compress', 'identity']); + expect(encodings).toEqual(['gzip', 'compress', 'identity']); }); it('parses header (exclude encodings)', () => { - const encodings = Accept.encodings('compress;q=0.5, gzip;q=0.0'); - expect(encodings).to.equal(['compress', 'identity']); + expect(encodings).toEqual(['compress', 'identity']); }); it('parses header (exclude identity)', () => { - const encodings = Accept.encodings('compress;q=0.5, gzip;q=1.0, identity;q=0'); - expect(encodings).to.equal(['gzip', 'compress']); + expect(encodings).toEqual(['gzip', 'compress']); }); }); diff --git a/test/index.js b/test/index.js index a90f4f2..7739806 100755 --- a/test/index.js +++ b/test/index.js @@ -1,24 +1,13 @@ -import * as Accept from '../lib/index.js'; -import * as Code from '@hapi/code'; -import * as Lab from '@hapi/lab'; +import { describe, expect, it } from 'vitest'; - -const internals = {}; - - -const lab = Lab.script(); -const { describe, it } = lab; -const expect = Code.expect; -export { lab }; +import * as Accept from '../src/index.js'; /* Accept-Charset: iso-8859-5, unicode-1-1;q=0.8 */ describe('parseAll()', () => { - it('parses all Accept headers', () => { - const headers = {}; headers.accept = 'text/plain, application/json;q=0.5, text/html;q=0.6, */*;q=0.1'; headers['accept-charset'] = 'iso-8859-5, unicode-1-1;q=0.8, *;q=0.001'; @@ -26,12 +15,12 @@ describe('parseAll()', () => { headers['accept-language'] = 'da, en;q=0.7, en-gb;q=0.8'; const accept = Accept.parseAll(headers); - expect(accept.isBoom).to.not.exist(); - expect(accept).to.equal({ + expect(accept.isBoom).toBeUndefined(); + expect(accept).toStrictEqual({ charsets: ['iso-8859-5', 'unicode-1-1', '*'], encodings: ['gzip', 'compress', 'identity'], languages: ['da', 'en-gb', 'en'], - mediaTypes: ['text/plain', 'text/html', 'application/json', '*/*'] + mediaTypes: ['text/plain', 'text/html', 'application/json', '*/*'], }); }); }); diff --git a/test/index.ts b/test/index.ts index 8b11f93..1bb0585 100755 --- a/test/index.ts +++ b/test/index.ts @@ -1,142 +1,212 @@ -import * as Accept from '../lib/index.js'; -import * as Lab from '@hapi/lab'; - - -const { expect } = Lab.types; - - -// charset() - -Accept.charset('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001'); -Accept.charset('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001', ['iso-8859-5']); -Accept.charset(''); -Accept.charset(); - -expect.type(Accept.charset('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001')); -expect.type(Accept.charset('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001', ['iso-8859-5'])); - -expect.error(Accept.charset(123)); -expect.error(Accept.charset('', '')); -expect.error(Accept.charset('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001', 'iso-8859-5')); - - -// charsets() - -Accept.charsets('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001'); -Accept.charsets(''); -Accept.charsets(); - -expect.type(Accept.charsets('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001')); - -expect.error(Accept.charsets(123)); -expect.error(Accept.charsets('', '')); -expect.error(Accept.charsets('', [''])); - - -// encoding() - -Accept.encoding('gzip;q=1.0, identity; Q=0.5, *;q=0'); -Accept.encoding('gzip;q=1.0, identity; Q=0.5, *;q=0', ['gzip']); -Accept.encoding(''); -Accept.encoding(); - -expect.type(Accept.encoding('gzip;q=1.0, identity; Q=0.5, *;q=0')); -expect.type(Accept.encoding('gzip;q=1.0, identity; Q=0.5, *;q=0', ['gzip'])); - -expect.error(Accept.encoding(123)); -expect.error(Accept.encoding('', '')); -expect.error(Accept.encoding('gzip;q=1.0, identity; Q=0.5, *;q=0', 'gzip')); - - -// encodings() - -Accept.encodings('gzip;q=1.0, identity; Q=0.5, *;q=0'); -Accept.encodings(''); -Accept.encodings(); - -expect.type(Accept.encodings('gzip;q=1.0, identity; Q=0.5, *;q=0')); - -expect.error(Accept.encodings(123)); -expect.error(Accept.encodings('', '')); -expect.error(Accept.encodings('', [''])); - - -// language() - -Accept.language('en;q=0.6, en-GB;q=0.8'); -Accept.language('en;q=0.6, en-GB;q=0.8', ['en']); -Accept.language(''); -Accept.language(); - -expect.type(Accept.language('en;q=0.6, en-GB;q=0.8')); -expect.type(Accept.language('en;q=0.6, en-GB;q=0.8', ['en'])); - -expect.error(Accept.language(123)); -expect.error(Accept.language('', '')); -expect.error(Accept.language('en;q=0.6, en-GB;q=0.8', 'en')); - - -// languages() - -Accept.languages('en;q=0.6, en-GB;q=0.8'); -Accept.languages(''); -Accept.languages(); - -expect.type(Accept.languages('en;q=0.6, en-GB;q=0.8')); - -expect.error(Accept.languages(123)); -expect.error(Accept.languages('', '')); -expect.error(Accept.languages('', [''])); - - -// mediaType() - -Accept.mediaType('application/json;q=0.6, text/plain;q=0.8'); -Accept.mediaType('application/json;q=0.6, text/plain;q=0.8', ['test/plain']); -Accept.mediaType(''); -Accept.mediaType(); - -expect.type(Accept.mediaType('application/json;q=0.6, text/plain;q=0.8')); -expect.type(Accept.mediaType('application/json;q=0.6, text/plain;q=0.8', ['test/plain'])); - -expect.error(Accept.mediaType(123)); -expect.error(Accept.mediaType('', '')); -expect.error(Accept.mediaType('application/json;q=0.6, text/plain;q=0.8', 'test/plain')); - - -// mediaTypes() - -Accept.mediaTypes('application/json;q=0.6, text/plain;q=0.8'); -Accept.mediaTypes('', ['text/plain']); -Accept.mediaTypes(''); -Accept.mediaTypes(); - -expect.type(Accept.mediaTypes('application/json;q=0.6, text/plain;q=0.8')); - -expect.error(Accept.mediaTypes(123)); -expect.error(Accept.mediaTypes('', '')); - - -// parseAll() - -Accept.parseAll({}); -Accept.parseAll({ accept: '' }); - -const headers = { - accept: 'application/json;q=0.6, text/plain;q=0.8', - 'accept-charset': 'iso-8859-5, unicode-1-1;q=0.8, *;q=0.001', - 'accept-encoding': 'gzip;q=1.0, identity; Q=0.5, *;q=0', - 'accept-language': 'en;q=0.6, en-GB;q=0.8', - ignore: {} -}; - -const all = Accept.parseAll(headers); - -expect.type(all); -expect.type(all.charsets); -expect.type(all.encodings); -expect.type(all.languages); -expect.type(all.mediaTypes); - -expect.error(Accept.parseAll()); -expect.error(Accept.parseAll({ accept: {} })); +import { describe, expectTypeOf, it } from 'vitest'; + +import * as Accept from '../src/index.js'; + +// Negative cases assert through `toBeCallableWith`, which is erased at runtime — a bare +// `// @ts-expect-error`-ed call would still execute and throw, since vitest loads this file +// under both `include` and `typecheck.include`. + +describe('typings', () => { + describe('charset()', () => { + it('accepts an optional header and optional preferences', () => { + expectTypeOf(Accept.charset).toBeCallableWith(); + expectTypeOf(Accept.charset).toBeCallableWith(''); + expectTypeOf(Accept.charset).toBeCallableWith('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001', ['iso-8859-5']); + }); + + it('returns a string', () => { + expectTypeOf(Accept.charset('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001')).toEqualTypeOf(); + expectTypeOf( + Accept.charset('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001', ['iso-8859-5']), + ).toEqualTypeOf(); + }); + + it('rejects a non-string header and non-array preferences', () => { + // @ts-expect-error number is not assignable to string + expectTypeOf(Accept.charset).toBeCallableWith(123); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.charset).toBeCallableWith('', ''); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.charset).toBeCallableWith('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001', 'iso-8859-5'); + }); + }); + + describe('charsets()', () => { + it('accepts an optional header', () => { + expectTypeOf(Accept.charsets).toBeCallableWith(); + expectTypeOf(Accept.charsets).toBeCallableWith(''); + }); + + it('returns a string array', () => { + expectTypeOf(Accept.charsets('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001')).toEqualTypeOf(); + }); + + it('rejects a non-string header and any second argument', () => { + // @ts-expect-error number is not assignable to string + expectTypeOf(Accept.charsets).toBeCallableWith(123); + // @ts-expect-error charsets() takes no preferences + expectTypeOf(Accept.charsets).toBeCallableWith('', ''); + // @ts-expect-error charsets() takes no preferences + expectTypeOf(Accept.charsets).toBeCallableWith('', ['']); + }); + }); + + describe('encoding()', () => { + it('accepts an optional header and optional preferences', () => { + expectTypeOf(Accept.encoding).toBeCallableWith(); + expectTypeOf(Accept.encoding).toBeCallableWith(''); + expectTypeOf(Accept.encoding).toBeCallableWith('gzip;q=1.0, identity; Q=0.5, *;q=0', ['gzip']); + }); + + it('returns a string', () => { + expectTypeOf(Accept.encoding('gzip;q=1.0, identity; Q=0.5, *;q=0')).toEqualTypeOf(); + expectTypeOf(Accept.encoding('gzip;q=1.0, identity; Q=0.5, *;q=0', ['gzip'])).toEqualTypeOf(); + }); + + it('rejects a non-string header and non-array preferences', () => { + // @ts-expect-error number is not assignable to string + expectTypeOf(Accept.encoding).toBeCallableWith(123); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.encoding).toBeCallableWith('', ''); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.encoding).toBeCallableWith('gzip;q=1.0, identity; Q=0.5, *;q=0', 'gzip'); + }); + }); + + describe('encodings()', () => { + it('accepts an optional header', () => { + expectTypeOf(Accept.encodings).toBeCallableWith(); + expectTypeOf(Accept.encodings).toBeCallableWith(''); + }); + + it('returns a string array', () => { + expectTypeOf(Accept.encodings('gzip;q=1.0, identity; Q=0.5, *;q=0')).toEqualTypeOf(); + }); + + it('rejects a non-string header and any second argument', () => { + // @ts-expect-error number is not assignable to string + expectTypeOf(Accept.encodings).toBeCallableWith(123); + // @ts-expect-error encodings() takes no preferences + expectTypeOf(Accept.encodings).toBeCallableWith('', ''); + // @ts-expect-error encodings() takes no preferences + expectTypeOf(Accept.encodings).toBeCallableWith('', ['']); + }); + }); + + describe('language()', () => { + it('accepts an optional header and optional preferences', () => { + expectTypeOf(Accept.language).toBeCallableWith(); + expectTypeOf(Accept.language).toBeCallableWith(''); + expectTypeOf(Accept.language).toBeCallableWith('en;q=0.6, en-GB;q=0.8', ['en']); + }); + + it('returns a string', () => { + expectTypeOf(Accept.language('en;q=0.6, en-GB;q=0.8')).toEqualTypeOf(); + expectTypeOf(Accept.language('en;q=0.6, en-GB;q=0.8', ['en'])).toEqualTypeOf(); + }); + + it('rejects a non-string header and non-array preferences', () => { + // @ts-expect-error number is not assignable to string + expectTypeOf(Accept.language).toBeCallableWith(123); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.language).toBeCallableWith('', ''); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.language).toBeCallableWith('en;q=0.6, en-GB;q=0.8', 'en'); + }); + }); + + describe('languages()', () => { + it('accepts an optional header', () => { + expectTypeOf(Accept.languages).toBeCallableWith(); + expectTypeOf(Accept.languages).toBeCallableWith(''); + }); + + it('returns a string array', () => { + expectTypeOf(Accept.languages('en;q=0.6, en-GB;q=0.8')).toEqualTypeOf(); + }); + + it('rejects a non-string header and any second argument', () => { + // @ts-expect-error number is not assignable to string + expectTypeOf(Accept.languages).toBeCallableWith(123); + // @ts-expect-error languages() takes no preferences + expectTypeOf(Accept.languages).toBeCallableWith('', ''); + // @ts-expect-error languages() takes no preferences + expectTypeOf(Accept.languages).toBeCallableWith('', ['']); + }); + }); + + describe('mediaType()', () => { + it('accepts an optional header and optional preferences', () => { + expectTypeOf(Accept.mediaType).toBeCallableWith(); + expectTypeOf(Accept.mediaType).toBeCallableWith(''); + expectTypeOf(Accept.mediaType).toBeCallableWith('application/json;q=0.6, text/plain;q=0.8', ['test/plain']); + }); + + it('returns a string', () => { + expectTypeOf(Accept.mediaType('application/json;q=0.6, text/plain;q=0.8')).toEqualTypeOf(); + expectTypeOf( + Accept.mediaType('application/json;q=0.6, text/plain;q=0.8', ['test/plain']), + ).toEqualTypeOf(); + }); + + it('rejects a non-string header and non-array preferences', () => { + // @ts-expect-error number is not assignable to string + expectTypeOf(Accept.mediaType).toBeCallableWith(123); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.mediaType).toBeCallableWith('', ''); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.mediaType).toBeCallableWith('application/json;q=0.6, text/plain;q=0.8', 'test/plain'); + }); + }); + + describe('mediaTypes()', () => { + it('accepts an optional header and optional preferences', () => { + expectTypeOf(Accept.mediaTypes).toBeCallableWith(); + expectTypeOf(Accept.mediaTypes).toBeCallableWith(''); + expectTypeOf(Accept.mediaTypes).toBeCallableWith('', ['text/plain']); + }); + + it('returns a string array', () => { + expectTypeOf(Accept.mediaTypes('application/json;q=0.6, text/plain;q=0.8')).toEqualTypeOf(); + }); + + it('rejects a non-string header and non-array preferences', () => { + // @ts-expect-error number is not assignable to string + expectTypeOf(Accept.mediaTypes).toBeCallableWith(123); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.mediaTypes).toBeCallableWith('', ''); + }); + }); + + describe('parseAll()', () => { + it('accepts a headers object', () => { + expectTypeOf(Accept.parseAll).toBeCallableWith({}); + expectTypeOf(Accept.parseAll).toBeCallableWith({ accept: '' }); + }); + + it('returns a result of string arrays', () => { + const headers = { + accept: 'application/json;q=0.6, text/plain;q=0.8', + 'accept-charset': 'iso-8859-5, unicode-1-1;q=0.8, *;q=0.001', + 'accept-encoding': 'gzip;q=1.0, identity; Q=0.5, *;q=0', + 'accept-language': 'en;q=0.6, en-GB;q=0.8', + ignore: {}, + }; + + const all = Accept.parseAll(headers); + + expectTypeOf(all).toEqualTypeOf(); + expectTypeOf(all.charsets).toEqualTypeOf(); + expectTypeOf(all.encodings).toEqualTypeOf(); + expectTypeOf(all.languages).toEqualTypeOf(); + expectTypeOf(all.mediaTypes).toEqualTypeOf(); + }); + + it('requires headers with string-typed accept values', () => { + // @ts-expect-error headers is required + expectTypeOf(Accept.parseAll).toBeCallableWith(); + // @ts-expect-error object is not assignable to string + expectTypeOf(Accept.parseAll).toBeCallableWith({ accept: {} }); + }); + }); +}); diff --git a/test/language.js b/test/language.js index cef266c..a12b9d8 100755 --- a/test/language.js +++ b/test/language.js @@ -1,153 +1,118 @@ -import * as Accept from '../lib/index.js'; -import * as Code from '@hapi/code'; -import * as Lab from '@hapi/lab'; - - -const internals = {}; - - -const lab = Lab.script(); -const { describe, it } = lab; -const expect = Code.expect; -export { lab }; +import { describe, expect, it } from 'vitest'; +import * as Accept from '../src/index.js'; describe('language()', () => { - it('parses the header', () => { - const language = Accept.language('da, en-GB, en'); - expect(language).to.equal('da'); + expect(language).toBe('da'); }); it('respects weights', () => { - const language = Accept.language('en;q=0.6, en-GB;q=0.8'); - expect(language).to.equal('en-gb'); + expect(language).toBe('en-gb'); }); it('requires the preferences parameter to be an array', () => { - expect(() => { - Accept.language('en;q=0.6, en-GB;q=0.8', 'en'); - }).to.throw('Preferences must be an array'); + }).toThrow('Preferences must be an array'); }); it('returns empty string with header is empty', () => { - const language = Accept.language(''); - expect(language).to.equal(''); + expect(language).toBe(''); }); it('returns empty string if header is missing', () => { - const language = Accept.language(); - expect(language).to.equal(''); + expect(language).toBe(''); }); it('ignores an empty preferences array', () => { - const language = Accept.language('da, en-GB, en', []); - expect(language).to.equal('da'); + expect(language).toBe('da'); }); it('returns empty string if none of the preferences match', () => { - const language = Accept.language('da, en-GB, en', ['es']); - expect(language).to.equal(''); + expect(language).toBe(''); }); it('returns first preference if header has * and is unmatched', () => { - const language = Accept.language('da, en-GB, *', ['en-US']); - expect(language).to.equal('en-US'); + expect(language).toBe('en-US'); }); it('returns first found preference that header includes', () => { - const language = Accept.language('da, en-GB, en', ['en-US', 'en-GB']); - expect(language).to.equal('en-US'); + expect(language).toBe('en-US'); }); it('returns preference with highest order when equal weigths', () => { - - expect(Accept.language('da, en, en-GB', ['en', 'en-GB'])).to.equal('en'); - expect(Accept.language('da, en, en-GB', ['en-GB', 'en'])).to.equal('en-GB'); - expect(Accept.language('en, en-GB, en-US')).to.equal('en'); + expect(Accept.language('da, en, en-GB', ['en', 'en-GB'])).toBe('en'); + expect(Accept.language('da, en, en-GB', ['en-GB', 'en'])).toBe('en-GB'); + expect(Accept.language('en, en-GB, en-US')).toBe('en'); }); it('return language with heighest weight', () => { - const language = Accept.language('da;q=0.5, en;q=1', ['da', 'en']); - expect(language).to.equal('en'); + expect(language).toBe('en'); }); it('ignores preference case when matching', () => { - const language = Accept.language('da, en-GB, en-us', ['en-gb', 'en-us']); // en-GB vs en-gb - expect(language).to.equal('en-gb'); + expect(language).toBe('en-gb'); }); it('returns language using range match', () => { - - expect(Accept.language('da', ['da-DK'])).to.equal('da-DK'); - expect(Accept.language('en-US, en', ['en-GB', 'en-US'])).to.equal('en-GB'); - expect(Accept.language('da, en', ['da-DK', 'en-GB'])).to.equal('da-DK'); - expect(Accept.language('en, da', ['da-DK', 'en-GB'])).to.equal('da-DK'); - expect(Accept.language('en, da', ['en', 'en-GB'])).to.equal('en'); - expect(Accept.language('da, en-GB', ['da-DK', 'en-GB'])).to.equal('da-DK'); - expect(Accept.language('en, en-GB', ['en-US', 'en-GB', 'da-DK'])).to.equal('en-US'); + expect(Accept.language('da', ['da-DK'])).toBe('da-DK'); + expect(Accept.language('en-US, en', ['en-GB', 'en-US'])).toBe('en-GB'); + expect(Accept.language('da, en', ['da-DK', 'en-GB'])).toBe('da-DK'); + expect(Accept.language('en, da', ['da-DK', 'en-GB'])).toBe('da-DK'); + expect(Accept.language('en, da', ['en', 'en-GB'])).toBe('en'); + expect(Accept.language('da, en-GB', ['da-DK', 'en-GB'])).toBe('da-DK'); + expect(Accept.language('en, en-GB', ['en-US', 'en-GB', 'da-DK'])).toBe('en-US'); }); it('explicit preference overrides range match', () => { - - expect(Accept.language('da, en-GB', ['da-DK', 'en-GB', 'da'])).to.equal('en-GB'); + expect(Accept.language('da, en-GB', ['da-DK', 'en-GB', 'da'])).toBe('en-GB'); }); }); - describe('languages()', () => { - it('parses the header', () => { - const languages = Accept.languages('da, en-GB, en'); - expect(languages).to.equal(['da', 'en-gb', 'en']); + expect(languages).toEqual(['da', 'en-gb', 'en']); }); it('orders by weight(q)', () => { - const languages = Accept.languages('da, en;q=0.7, en-GB;q=0.8'); - expect(languages).to.equal(['da', 'en-gb', 'en']); + expect(languages).toEqual(['da', 'en-gb', 'en']); }); it('maintains case', () => { - const languages = Accept.languages('da, en-GB, en'); - expect(languages).to.equal(['da', 'en-gb', 'en']); + expect(languages).toEqual(['da', 'en-gb', 'en']); }); it('drops zero weighted charsets', () => { - const languages = Accept.languages('da, en-GB, es;q=0, en'); - expect(languages).to.equal(['da', 'en-gb', 'en']); + expect(languages).toEqual(['da', 'en-gb', 'en']); }); it('ignores invalid weights', () => { - const languages = Accept.languages('da, en-GB;q=1.1, es;q=a, en;q=0.0001'); - expect(languages).to.equal(['da', 'en-gb', 'es', 'en']); + expect(languages).toEqual(['da', 'en-gb', 'es', 'en']); }); it('return empty array when no header is present', () => { - const languages = Accept.languages(); - expect(languages).to.equal([]); + expect(languages).toEqual([]); }); it('return empty array when header is empty', () => { - const languages = Accept.languages(''); - expect(languages).to.equal([]); + expect(languages).toEqual([]); }); }); diff --git a/test/mediatype.js b/test/mediatype.js index 415e72f..fd9849e 100755 --- a/test/mediatype.js +++ b/test/mediatype.js @@ -1,151 +1,121 @@ -import * as Accept from '../lib/index.js'; -import * as Code from '@hapi/code'; -import * as Lab from '@hapi/lab'; - - -const internals = {}; - - -const lab = Lab.script(); -const { describe, it } = lab; -const expect = Code.expect; -export { lab }; +import { describe, expect, it } from 'vitest'; +import * as Accept from '../src/index.js'; // Mediatypes describe('mediaType()', () => { - it('parses the header', () => { - const mediaType = Accept.mediaType(',text/html, text/plain,, application/json'); - expect(mediaType).to.equal('application/json'); + expect(mediaType).toBe('application/json'); }); it('respects weights', () => { - const mediaType = Accept.mediaType('application/json;q=0.6, text/plain;q=0.8'); - expect(mediaType).to.equal('text/plain'); + expect(mediaType).toBe('text/plain'); }); it('requires the preferences parameter to be an array', () => { - expect(() => { - Accept.mediaType('application/json;q=0.6, text/plain;q=0.8', 'text/plain'); - }).to.throw('Preferences must be an array'); + }).toThrow('Preferences must be an array'); }); it('returns */* with header is empty', () => { - const mediaType = Accept.mediaType(''); - expect(mediaType).to.equal('*/*'); + expect(mediaType).toBe('*/*'); }); it('returns */* if header is missing', () => { - const mediaType = Accept.mediaType(); - expect(mediaType).to.equal('*/*'); + expect(mediaType).toBe('*/*'); }); it('ignores an empty preferences array', () => { - const mediaType = Accept.mediaType('text/plain, text/html, application/json', []); - expect(mediaType).to.equal('application/json'); + expect(mediaType).toBe('application/json'); }); it('returns empty string if none of the preferences match', () => { - const mediaType = Accept.mediaType('text/csv, application/json, text/plain', ['text/html']); - expect(mediaType).to.equal(''); + expect(mediaType).toBe(''); }); it('returns first preference if header has */*', () => { - const mediaType = Accept.mediaType('text/html, application/json, text/plain, */*', ['text/csv']); - expect(mediaType).to.equal('text/csv'); + expect(mediaType).toBe('text/csv'); }); it('returns first found preference that header includes', () => { - const mediaType = Accept.mediaType('text/html, application/json, text/plain', ['text/csv', 'application/json']); - expect(mediaType).to.equal('application/json'); + expect(mediaType).toBe('application/json'); }); it('returns preference with highest specificity', () => { - const mediaType = Accept.mediaType('text/*, text/html, application/json', ['text/plain', 'text/html']); - expect(mediaType).to.equal('text/html'); + expect(mediaType).toBe('text/html'); }); it('return media type with highest weight', () => { - const mediaType = Accept.mediaType('text/html;q=0.5, text/plain;q=1', ['text/html', 'text/plain']); - expect(mediaType).to.equal('text/plain'); + expect(mediaType).toBe('text/plain'); }); it('prioritizes q value over wildcard', () => { - - expect(Accept.mediaType('text/plain;q=0.1, image/*;q=1', ['text/plain', 'image/jpeg'])).to.equal('image/jpeg'); + expect(Accept.mediaType('text/plain;q=0.1, image/*;q=1', ['text/plain', 'image/jpeg'])).toBe('image/jpeg'); }); it('removes extension parameters', () => { - - expect(Accept.mediaTypes('text/html;charset="ascii";q=0.5;ext=123, text/plain;other="xy z";Q=1')).to.equal([ + expect(Accept.mediaTypes('text/html;charset="ascii";q=0.5;ext=123, text/plain;other="xy z";Q=1')).toEqual([ 'text/plain;other="xy z"', - 'text/html;charset="ascii"' + 'text/html;charset="ascii"', ]); }); it('errors on invalid params', () => { - - expect(() => Accept.mediaType('text/plain;q')).to.throw(); - expect(() => Accept.mediaType('text/plain;q=')).to.throw(); + expect(() => Accept.mediaType('text/plain;q')).toThrow(); + expect(() => Accept.mediaType('text/plain;q=')).toThrow(); }); }); describe('mediaTypes()', () => { - it('returns */* when header is missing', () => { - - expect(Accept.mediaTypes()).to.equal(['*/*']); + expect(Accept.mediaTypes()).toEqual(['*/*']); }); it('parses header', () => { - const mediaTypes = Accept.mediaTypes('text/plain, application/json;q=0.5, text/html, */*;q=0.1, audio/*'); - expect(mediaTypes).to.equal(['audio/*', 'text/html', 'text/plain', 'application/json', '*/*']); + expect(mediaTypes).toEqual(['audio/*', 'text/html', 'text/plain', 'application/json', '*/*']); }); it('parses header with preferences', () => { - - const mediaTypes = Accept.mediaTypes('text/plain, application/json;q=0.5, text/html, audio/*', ['text/*', 'application/*']); - expect(mediaTypes).to.equal(['text/html', 'text/plain', 'application/json']); + const mediaTypes = Accept.mediaTypes('text/plain, application/json;q=0.5, text/html, audio/*', [ + 'text/*', + 'application/*', + ]); + expect(mediaTypes).toEqual(['text/html', 'text/plain', 'application/json']); }); it('returns empty array when everything is disallowed', () => { - const mediaTypes = Accept.mediaTypes('*/*;q=0'); - expect(mediaTypes).to.equal([]); + expect(mediaTypes).toEqual([]); }); it('respects disallows', () => { - const mediaTypes = Accept.mediaTypes('text/plain, application/json;q=0.5, text/html, text/drop;q=0'); - expect(mediaTypes).to.equal(['text/html', 'text/plain', 'application/json']); + expect(mediaTypes).toEqual(['text/html', 'text/plain', 'application/json']); }); it('orders by weight', () => { - const mediaTypes = Accept.mediaTypes('application/json;q=0.2, text/html'); - expect(mediaTypes).to.equal(['text/html', 'application/json']); + expect(mediaTypes).toEqual(['text/html', 'application/json']); }); it('orders most specific to least specific', () => { - - const types = 'text/*, text/plain;format=flowed, text/plain, text/plain;level=1, text/html, text/plain;level=2, */*, image/*, text/rich'; + const types = + 'text/*, text/plain;format=flowed, text/plain, text/plain;level=1, text/html, text/plain;level=2, */*, image/*, text/rich'; const mediaTypes = Accept.mediaTypes(types); - expect(mediaTypes).to.equal([ + expect(mediaTypes).toEqual([ 'image/*', 'text/html', 'text/plain;format=flowed', @@ -154,47 +124,46 @@ describe('mediaTypes()', () => { 'text/plain', 'text/rich', 'text/*', - '*/*' + '*/*', ]); }); it('match subtype to preference', () => { - - expect(Accept.mediaTypes('text/html', ['TEXT/*'])).to.equal(['text/html']); + expect(Accept.mediaTypes('text/html', ['TEXT/*'])).toEqual(['text/html']); }); it('keeps wildcard behind more specific', () => { - - expect(Accept.mediaTypes('text/html, text/*')).to.equal(['text/html', 'text/*']); - expect(Accept.mediaTypes('text/*, text/html')).to.equal(['text/html', 'text/*']); + expect(Accept.mediaTypes('text/html, text/*')).toEqual(['text/html', 'text/*']); + expect(Accept.mediaTypes('text/*, text/html')).toEqual(['text/html', 'text/*']); }); it('matches */* preference to anything', () => { - - expect(Accept.mediaTypes('text/html, text/*, */*', ['*/*'])).to.equal(['text/html', 'text/*', '*/*']); - expect(Accept.mediaTypes('text/html, text/*, */*', ['*/*', 'TEXT/html'])).to.equal(['TEXT/html', 'text/*', '*/*']); + expect(Accept.mediaTypes('text/html, text/*, */*', ['*/*'])).toEqual(['text/html', 'text/*', '*/*']); + expect(Accept.mediaTypes('text/html, text/*, */*', ['*/*', 'TEXT/html'])).toEqual([ + 'TEXT/html', + 'text/*', + '*/*', + ]); }); it('keeps the order of two media types with extensions', () => { - const mediaTypes = Accept.mediaTypes('text/html;level=1, text/html;level=2'); - expect(mediaTypes).to.equal(['text/html;level=1', 'text/html;level=2']); + expect(mediaTypes).toEqual(['text/html;level=1', 'text/html;level=2']); }); it('invalid header returns []', () => { - const mediaTypes = Accept.mediaTypes('ab'); - expect(mediaTypes).to.equal([]); + expect(mediaTypes).toEqual([]); }); it('invalid weight is ignored', () => { - const mediaTypes = Accept.mediaTypes('text/html;q=0.0001, text/plain;q=1.1, text/csv;q=a'); - expect(mediaTypes).to.equal(['text/csv', 'text/html', 'text/plain']); + expect(mediaTypes).toEqual(['text/csv', 'text/html', 'text/plain']); }); it('errors on invalid preference', () => { - - expect(() => Accept.mediaTypes('text/html', ['*/html'])).to.throw('Invalid media type preference contains wildcard type with a subtype'); + expect(() => Accept.mediaTypes('text/html', ['*/html'])).toThrow( + 'Invalid media type preference contains wildcard type with a subtype', + ); }); }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..849762f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": true, + "isolatedDeclarations": true, + "isolatedModules": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ESNext" + } +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..2420cb5 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,23 @@ +import Oxc from '@hapi/oxc-plugin/vitest'; +import { defineConfig } from 'vitest/config'; + +import type { ViteUserConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [Oxc()], + test: { + environment: 'node', + include: ['test/**/*.{js,ts}'], + typecheck: { + enabled: true, + include: ['test/**/*.{js,ts}'], + }, + coverage: { + provider: 'v8', + include: ['src/**'], + thresholds: { + 100: true, + }, + }, + }, +}) as ViteUserConfig; From e9b144f453f418256d3bc95b72670f55344f73b4 Mon Sep 17 00:00:00 2001 From: Danilo Alonso Date: Sat, 25 Jul 2026 04:22:02 -0400 Subject: [PATCH 3/7] refactor: drop internals namespace ESM module scope already makes these private; exporting nothing is what keeps them so. media.js's `internals.preferences` becomes `filterPreferences` to avoid shadowing the `preferences` parameter. --- src/header.js | 11 ++++------- src/index.js | 44 +++++++++++++++++++++----------------------- src/media.js | 29 +++++++++++++---------------- 3 files changed, 38 insertions(+), 46 deletions(-) diff --git a/src/header.js b/src/header.js index 471c58b..69d52b9 100755 --- a/src/header.js +++ b/src/header.js @@ -2,9 +2,6 @@ import * as Hoek from '@hapi/hoek'; import * as Boom from '@hapi/boom'; -const internals = {}; - - const selection = function (header, preferences, options) { const results = selections(header, preferences, options); @@ -16,7 +13,7 @@ const selections = function (header, preferences, options) { Hoek.assert(!preferences || Array.isArray(preferences), 'Preferences must be an array'); - return internals.parse(header || '', preferences, options); + return parse(header || '', preferences, options); }; @@ -69,7 +66,7 @@ const selections = function (header, preferences, options) { // qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ) -internals.parse = function (raw, preferences, options) { +const parse = function (raw, preferences, options) { // Normalize header (remove spaces and tabs) @@ -168,7 +165,7 @@ internals.parse = function (raw, preferences, options) { // Sort selection based on q and then position in header - results.sort(internals.sort); + results.sort(sort); // Extract tokens @@ -205,7 +202,7 @@ internals.parse = function (raw, preferences, options) { }; -internals.sort = function (a, b) { +const sort = function (a, b) { const aFirst = -1; const bFirst = 1; diff --git a/src/index.js b/src/index.js index 6d5dada..5797174 100755 --- a/src/index.js +++ b/src/index.js @@ -2,38 +2,36 @@ import * as Header from './header.js'; import * as Media from './media.js'; -const internals = { - options: { - charset: { - type: 'accept-charset' - }, - encoding: { - type: 'accept-encoding', - default: 'identity', - equivalents: new Map([ - ['x-compress', 'compress'], - ['x-gzip', 'gzip'] - ]) - }, - language: { - type: 'accept-language', - prefixMatch: true - } +const options = { + charset: { + type: 'accept-charset' + }, + encoding: { + type: 'accept-encoding', + default: 'identity', + equivalents: new Map([ + ['x-compress', 'compress'], + ['x-gzip', 'gzip'] + ]) + }, + language: { + type: 'accept-language', + prefixMatch: true } }; -const charset = (header, preferences) => Header.selection(header, preferences, internals.options.charset); +const charset = (header, preferences) => Header.selection(header, preferences, options.charset); -const charsets = (header, preferences) => Header.selections(header, preferences, internals.options.charset); +const charsets = (header, preferences) => Header.selections(header, preferences, options.charset); -const encoding = (header, preferences) => Header.selection(header, preferences, internals.options.encoding); +const encoding = (header, preferences) => Header.selection(header, preferences, options.encoding); -const encodings = (header, preferences) => Header.selections(header, preferences, internals.options.encoding); +const encodings = (header, preferences) => Header.selections(header, preferences, options.encoding); -const language = (header, preferences) => Header.selection(header, preferences, internals.options.language); +const language = (header, preferences) => Header.selection(header, preferences, options.language); -const languages = (header, preferences) => Header.selections(header, preferences, internals.options.language); +const languages = (header, preferences) => Header.selections(header, preferences, options.language); const mediaType = (header, preferences) => Media.selection(header, preferences); diff --git a/src/media.js b/src/media.js index 4549778..34d4f91 100755 --- a/src/media.js +++ b/src/media.js @@ -2,9 +2,6 @@ import * as Hoek from '@hapi/hoek'; import * as Boom from '@hapi/boom'; -const internals = {}; - - const selection = function (header, preferences) { const results = selections(header, preferences); @@ -16,7 +13,7 @@ const selections = function (header, preferences) { Hoek.assert(!preferences || Array.isArray(preferences), 'Preferences must be an array'); - return internals.parse(header, preferences); + return parse(header, preferences); }; @@ -59,14 +56,14 @@ const selections = function (header, preferences) { // */* type/* type/subtype -internals.validMediaRx = /^(?:\*\/\*)|(?:[\w\!#\$%&'\*\+\-\.\^`\|~]+\/\*)|(?:[\w\!#\$%&'\*\+\-\.\^`\|~]+\/[\w\!#\$%&'\*\+\-\.\^`\|~]+)$/; +const validMediaRx = /^(?:\*\/\*)|(?:[\w\!#\$%&'\*\+\-\.\^`\|~]+\/\*)|(?:[\w\!#\$%&'\*\+\-\.\^`\|~]+\/[\w\!#\$%&'\*\+\-\.\^`\|~]+)$/; -internals.parse = function (raw, preferences) { +const parse = function (raw, preferences) { // Normalize header (remove spaces and temporary remove quoted strings) - const { header, quoted } = internals.normalize(raw); + const { header, quoted } = normalize(raw); // Parse selections @@ -85,7 +82,7 @@ internals.parse = function (raw, preferences) { const pairs = part.split(';'); const token = pairs.shift().toLowerCase(); - if (!internals.validMediaRx.test(token)) { // Ignore invalid types + if (!validMediaRx.test(token)) { // Ignore invalid types continue; } @@ -155,13 +152,13 @@ internals.parse = function (raw, preferences) { // Sort selection based on q and then position in header - results.sort(internals.sort); + results.sort(sort); - return internals.preferences(map, results, preferences); + return filterPreferences(map, results, preferences); }; -internals.normalize = function (raw) { +const normalize = function (raw) { raw = raw || '*/*'; @@ -185,7 +182,7 @@ internals.normalize = function (raw) { }; -internals.sort = function (a, b) { +const sort = function (a, b) { // Sort by quality score @@ -196,13 +193,13 @@ internals.sort = function (a, b) { // Sort by type if (a.type !== b.type) { - return internals.innerSort(a, b, 'type'); + return innerSort(a, b, 'type'); } // Sort by subtype if (a.subtype !== b.subtype) { - return internals.innerSort(a, b, 'subtype'); + return innerSort(a, b, 'subtype'); } // Sort by specificity @@ -215,7 +212,7 @@ internals.sort = function (a, b) { }; -internals.innerSort = function (a, b, key) { +const innerSort = function (a, b, key) { const aFirst = -1; const bFirst = 1; @@ -232,7 +229,7 @@ internals.innerSort = function (a, b, key) { }; -internals.preferences = function (map, results, preferences) { +const filterPreferences = function (map, results, preferences) { // Return selections if no preferences From d5e5e33b25cdef94a8cb296698a6378d4f8aff74 Mon Sep 17 00:00:00 2001 From: Danilo Alonso Date: Sat, 25 Jul 2026 04:23:09 -0400 Subject: [PATCH 4/7] style: reformat with oxfmt --- .github/workflows/ci-module.yml | 14 +++---- API.md | 29 ++++++------- README.md | 2 +- package.json | 4 +- src/header.js | 46 ++++++--------------- src/index.d.ts | 73 ++++++++++++--------------------- src/index.js | 17 +++----- src/media.js | 66 +++++++++++------------------ 8 files changed, 91 insertions(+), 160 deletions(-) diff --git a/.github/workflows/ci-module.yml b/.github/workflows/ci-module.yml index 0581710..4b7ee12 100644 --- a/.github/workflows/ci-module.yml +++ b/.github/workflows/ci-module.yml @@ -1,12 +1,12 @@ name: ci on: - push: - branches: - - master - pull_request: - workflow_dispatch: + push: + branches: + - master + pull_request: + workflow_dispatch: jobs: - test: - uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-22-hapi-21 + test: + uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-22-hapi-21 diff --git a/API.md b/API.md index 4726a51..4115ef3 100755 --- a/API.md +++ b/API.md @@ -1,6 +1,6 @@ ## Introduction -Accept helps to answer the question of how best to respond to a HTTP request, based on the requesting browser's capabilities. Accept will parse the headers of a HTTP request and tell you what the preferred encoding is, what language should be used, and what charsets and media types are accepted. +Accept helps to answer the question of how best to respond to a HTTP request, based on the requesting browser's capabilities. Accept will parse the headers of a HTTP request and tell you what the preferred encoding is, what language should be used, and what charsets and media types are accepted. Additional details about Accept headers and content negotiation can be found in [IETF RFC 7231, Section 5.3](https://tools.ietf.org/html/rfc7231#section-5.3). @@ -8,7 +8,7 @@ Additional details about Accept headers and content negotiation can be found in ### `charset(charsetHeader, [preferences])` -Given a string of acceptable charsets from a HTTP request Accept-Charset header, and an optional array of charset preferences, it will return a string indicating the best charset option that can be used in the HTTP response. This takes into account any weighting parameters given in the header for ordering and exclusion. +Given a string of acceptable charsets from a HTTP request Accept-Charset header, and an optional array of charset preferences, it will return a string indicating the best charset option that can be used in the HTTP response. This takes into account any weighting parameters given in the header for ordering and exclusion. ``` const charset = Accept.charsets("iso-8859-5, unicode-1-1;q=0.8"); // charset === "iso-8859-5" @@ -26,7 +26,7 @@ const charsets = Accept.charsets("iso-8859-5;q=0.5, unicode-1-1;q=0.8"); // char ### `encoding(encodingHeader, [preferences])` -Given a string of acceptable encodings from a HTTP request Accept-Encoding header, and optionally an array of preferences, it will return a string with the best fit encoding that should be used in the HTTP response. If no preferences array parameter is given the highest weighted or first ordered encoding is returned. If weightings are given in the header (using the q parameter) they are taken into account and the highest weighted match is returned. If a preferences array is given the best match from the array is returned. For more information about how the preferences array works see the section below on [Preferences](#preferences). +Given a string of acceptable encodings from a HTTP request Accept-Encoding header, and optionally an array of preferences, it will return a string with the best fit encoding that should be used in the HTTP response. If no preferences array parameter is given the highest weighted or first ordered encoding is returned. If weightings are given in the header (using the q parameter) they are taken into account and the highest weighted match is returned. If a preferences array is given the best match from the array is returned. For more information about how the preferences array works see the section below on [Preferences](#preferences). ``` const encoding = Accept.encoding("gzip, deflate, sdch"); // encoding === "gzip" @@ -43,9 +43,9 @@ const encodings = Accept.encodings("compress;q=0.5, gzip;q=1.0"); // encodings = ### `language(languageHeader, [preferences])` -Given a string of acceptable language ranges from a HTTP request Accept-Language header, and an optional array of language-tag preferences, it will return a string indicating the best language that can be used in the HTTP response. It respects the [q weightings](#weightings) of the languages in the header, returning the matched preference with the highest weighting. The case of the preference does not have to match the case of the option in the header. +Given a string of acceptable language ranges from a HTTP request Accept-Language header, and an optional array of language-tag preferences, it will return a string indicating the best language that can be used in the HTTP response. It respects the [q weightings](#weightings) of the languages in the header, returning the matched preference with the highest weighting. The case of the preference does not have to match the case of the option in the header. -If preferences is missing or an empty array, the highest weighted language is returned. If no preference matches, an empty string is returned. +If preferences is missing or an empty array, the highest weighted language is returned. If no preference matches, an empty string is returned. ``` const language = Accept.language("en;q=0.7, en-GB;q=0.8"); // language === "en-gb" @@ -64,7 +64,7 @@ const languages = Accept.languages("da, en;q=0.7, en-GB;q=0.8"); // languages == ### `mediaType(mediaTypeHeader, [preferences])` -Given a string of acceptable media types from a HTTP request Accept header, and optionally an array of preferences, it will return a string with the best fit media type that should be used in the HTTP response. If no preferences array parameter is given the highest weighted or first ordered media type is returned. If weightings are given in the header (using the q parameter) they are taken into account and the highest weighted match is returned. If a preferences array is given the best match from the array is returned. For more information about how the preferences array works see the section below on [Preferences](#preferences). +Given a string of acceptable media types from a HTTP request Accept header, and optionally an array of preferences, it will return a string with the best fit media type that should be used in the HTTP response. If no preferences array parameter is given the highest weighted or first ordered media type is returned. If weightings are given in the header (using the q parameter) they are taken into account and the highest weighted match is returned. If a preferences array is given the best match from the array is returned. For more information about how the preferences array works see the section below on [Preferences](#preferences). ``` const mediaType = Accept.mediaType("text/plain, application/json;q=0.5, text/html, */*;q=0.1"); @@ -85,7 +85,7 @@ const mediaTypes = Accept.mediaTypes("text/plain, application/json;q=0.5, text/h ### `parseAll(headers)` -Given the headers from a Hapi request object, `parseAll()` will parse all of the Accepts-* headers it currently understands into an object. +Given the headers from a Hapi request object, `parseAll()` will parse all of the Accepts-\* headers it currently understands into an object. ``` const all = Accept.parseAll(request.headers); @@ -97,12 +97,11 @@ const all = Accept.parseAll(request.headers); // } ``` - ## Q Weightings -The Accept-* headers may optionally include preferential weighting to indicate which options are best for the requester. It does this with `q` parameters in the headers (which stands for quality). These q weightings must be in the range of 0 to 1, with a max of three decimal places. The weightings are used to order the data given in the header, with the highest number being most preferential. Anything with a q rating of 0 is not allowed at all. +The Accept-\* headers may optionally include preferential weighting to indicate which options are best for the requester. It does this with `q` parameters in the headers (which stands for quality). These q weightings must be in the range of 0 to 1, with a max of three decimal places. The weightings are used to order the data given in the header, with the highest number being most preferential. Anything with a q rating of 0 is not allowed at all. -If a particular Accept method allows a `preferences` array parameter, such as `encoding()`, the weightings in the header affect which preference will be returned. Your preferences are matched with the weighting in mind, and the highest weighted option will be returned, no matter what order you list your preferences. The header weighting is most important. +If a particular Accept method allows a `preferences` array parameter, such as `encoding()`, the weightings in the header affect which preference will be returned. Your preferences are matched with the weighting in mind, and the highest weighted option will be returned, no matter what order you list your preferences. The header weighting is most important. ``` const encoding = Accept.encoding("gzip;q=1.0, identity;q=0.5", ["identity", "gzip"]); @@ -110,18 +109,17 @@ const encoding = Accept.encoding("gzip;q=1.0, identity;q=0.5", ["identity", "gzi // despite identity getting listed first in the preferences array, gzip has a higher q weighting, so it is returned. ``` - ## Encodings ### Preferences -If you are looking for a set of specific encodings you can pass that in as an array to the `preferences` parameter. Your preferences **must** be an array. In the preferences array you specify a list of possible encodings you want to look for, in order of preference. Accept will return back the most preferential option it can find, if any match. The preferences array does not support parameters, only base types. +If you are looking for a set of specific encodings you can pass that in as an array to the `preferences` parameter. Your preferences **must** be an array. In the preferences array you specify a list of possible encodings you want to look for, in order of preference. Accept will return back the most preferential option it can find, if any match. The preferences array does not support parameters, only base types. ``` const encoding = Accept.encoding("gzip, deflate, sdch", ["deflate", "identity"]); // encoding === "delate" ``` -Your preferences are evaluated without any case sensitivity, to better match what the browser sends. This means that "gZip" will match a preference of ["gzip"]. +Your preferences are evaluated without any case sensitivity, to better match what the browser sends. This means that "gZip" will match a preference of ["gzip"]. ``` const encoding = Accept.encoding("gZip, deflate, sdch", ["gzip"]); // encoding === "gzip" @@ -133,13 +131,12 @@ If you supply a preferences array, and no match is found, `encoding()` will retu const encoding = Accept.encoding("gZip", ["deflate"]); // encoding === "" ``` -If the encoding header is the special "*" that indicates the browser will accept any encoding. In that case the top preference from your supplied options will be returned. +If the encoding header is the special "\*" that indicates the browser will accept any encoding. In that case the top preference from your supplied options will be returned. ``` const encoding = Accept.encoding("*", ["gzip"]); // encoding === "gzip" ``` - ### Identity When you ask Accept for a list of all the supported encodings from the request, using the `encodings()` function (plural, not singular), you will be returned an array of strings in order from most preferred to least as determined by the encoding weight. @@ -148,4 +145,4 @@ When you ask Accept for a list of all the supported encodings from the request, const encodings = Accept.encodings("compress;q=0.5, gzip;q=1.0"); // encodings === ["gzip", "compress", "identity"] ``` -You'll notice that `identity` was returned in the array, even though it's not in the encoding header. Identity is always an option for encoding, unless specifically excluded in the header using a weighting of zero. Identity just means respond with no special encoding. +You'll notice that `identity` was returned in the array, even though it's not in the encoding header. Identity is always an option for encoding, unless specifically excluded in the header using a weighting of zero. Identity just means respond with no special encoding. diff --git a/README.md b/README.md index 865de15..c4f6504 100755 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # @hapi/accept -#### HTTP Accept-* headers parsing. +#### HTTP Accept-\* headers parsing. **accept** is part of the **hapi** ecosystem and was designed to work seamlessly with the [hapi web framework](https://hapi.dev) and its other components (but works great on its own or with other frameworks). If you are using a different web framework and find this module useful, check out [hapi](https://hapi.dev) – they work even better together. diff --git a/package.json b/package.json index 5ba6d60..0b16d53 100755 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "description": "HTTP Accept-* headers parsing", "keywords": [ "HTTP", - "header", "accept", - "accept-encoding" + "accept-encoding", + "header" ], "license": "BSD-3-Clause", "repository": "git://github.com/hapijs/accept", diff --git a/src/header.js b/src/header.js index 69d52b9..1df0e81 100755 --- a/src/header.js +++ b/src/header.js @@ -1,22 +1,17 @@ -import * as Hoek from '@hapi/hoek'; import * as Boom from '@hapi/boom'; - +import * as Hoek from '@hapi/hoek'; const selection = function (header, preferences, options) { - const results = selections(header, preferences, options); return results.length ? results[0] : ''; }; - const selections = function (header, preferences, options) { - Hoek.assert(!preferences || Array.isArray(preferences), 'Preferences must be an array'); return parse(header || '', preferences, options); }; - // RFC 7231 Section 5.3.3 (https://tools.ietf.org/html/rfc7231#section-5.3.3) // // Accept-Charset = *( "," OWS ) ( ( charset / "*" ) [ weight ] ) *( OWS "," [ OWS ( ( charset / "*" ) [ weight ] ) ] ) @@ -24,7 +19,6 @@ const selections = function (header, preferences, options) { // // Accept-Charset: iso-8859-5, unicode-1-1;q=0.8 - // RFC 7231 Section 5.3.4 (https://tools.ietf.org/html/rfc7231#section-5.3.4) // // Accept-Encoding = [ ( "," / ( codings [ weight ] ) ) *( OWS "," [ OWS ( codings [ weight ] ) ] ) ] @@ -37,7 +31,6 @@ const selections = function (header, preferences, options) { // Accept-Encoding: compress;q=0.5, gzip;q=1.0 // Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0 - // RFC 7231 Section 5.3.5 (https://tools.ietf.org/html/rfc7231#section-5.3.5) // // Accept-Language = *( "," OWS ) ( language-range [ weight ] ) *( OWS "," [ OWS ( language-range [ weight ] ) ] ) @@ -46,7 +39,6 @@ const selections = function (header, preferences, options) { // // Accept-Language: da, en-gb;q=0.8, en;q=0.7 - // token = 1*tchar // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" @@ -54,7 +46,6 @@ const selections = function (header, preferences, options) { // ; any VCHAR, except delimiters // OWS = *( SP / HTAB ) - // RFC 7231 Section 5.3.1 (https://tools.ietf.org/html/rfc7231#section-5.3.1) // // The weight is normalized to a real number in the range 0 through 1, @@ -65,9 +56,7 @@ const selections = function (header, preferences, options) { // weight = OWS ";" OWS "q=" qvalue // qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ) - const parse = function (raw, preferences, options) { - // Normalize header (remove spaces and tabs) const header = raw.replace(/[ \t]/g, ''); @@ -83,7 +72,7 @@ const parse = function (raw, preferences, options) { if (options.prefixMatch) { const parts = lower.split('-'); - while (parts.pop(), parts.length > 0) { + while ((parts.pop(), parts.length > 0)) { const joined = parts.join('-'); if (!lowers.has(joined)) { lowers.set(joined, { orig: preference, pos: pos++ }); @@ -101,7 +90,9 @@ const parse = function (raw, preferences, options) { for (let i = 0; i < parts.length; ++i) { const part = parts[i]; - if (!part) { // Ignore empty parts or leading commas + // Ignore empty parts or leading commas + + if (!part) { continue; } @@ -124,12 +115,10 @@ const parse = function (raw, preferences, options) { const item = { token, pos: i, - q: 1 + q: 1, }; - if (preferences && - lowers.has(token)) { - + if (preferences && lowers.has(token)) { item.pref = lowers.get(token).pos; } @@ -141,9 +130,7 @@ const parse = function (raw, preferences, options) { const q = params[1]; const [key, value] = q.split('='); - if (!value || - key !== 'q' && key !== 'Q') { - + if (!value || (key !== 'q' && key !== 'Q')) { throw Boom.badRequest(`Invalid ${options.type} header`); } @@ -152,15 +139,12 @@ const parse = function (raw, preferences, options) { continue; } - if (Number.isFinite(score) && - score <= 1 && - score >= 0.001) { - + if (Number.isFinite(score) && score <= 1 && score >= 0.001) { item.q = score; } } - results.push(item); // Only add allowed selections (q !== 0) + results.push(item); // Only add allowed selections (q !== 0) } // Sort selection based on q and then position in header @@ -171,9 +155,7 @@ const parse = function (raw, preferences, options) { const values = results.map((item) => item.token); - if (options.default && - !map.has(options.default)) { - + if (options.default && !map.has(options.default)) { values.push(options.default); } @@ -189,8 +171,7 @@ const parse = function (raw, preferences, options) { preferred.push(prefValue.orig); } } - } - else { + } else { const lower = value.toLowerCase(); if (lowers.has(lower)) { preferred.push(lowers.get(lower).orig); @@ -201,9 +182,7 @@ const parse = function (raw, preferences, options) { return preferred; }; - const sort = function (a, b) { - const aFirst = -1; const bFirst = 1; @@ -226,5 +205,4 @@ const sort = function (a, b) { return a.pos - b.pos; }; - export { selection, selections }; diff --git a/src/index.d.ts b/src/index.d.ts index a9be6da..113189f 100755 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,101 +1,83 @@ /** * Identifies the best character-set for an HTTP response based on the HTTP request Accept-Charset header. - * - * @param header - the HTTP Accept-Charset header content. - * @param preferences - an optional array of character-set strings in order of server preference. - * - * @return a string with the preferred accepted character-set. + * + * @param header - The HTTP Accept-Charset header content. + * @param preferences - An optional array of character-set strings in order of server preference. + * @returns A string with the preferred accepted character-set. */ export function charset(header?: string, preferences?: readonly string[]): string; - /** - * Sorts the character-sets in the HTTP request Accept-Charset header based on client preference from most to least desired. - * - * @param header - the HTTP Accept-Charset header content. + * Sorts the character-sets in the HTTP request Accept-Charset header based on client preference from most to least + * desired. * - * @return an array of strings of character-sets sorted from the most to the least desired. + * @param header - The HTTP Accept-Charset header content. + * @returns An array of strings of character-sets sorted from the most to the least desired. */ export function charsets(header?: string): string[]; - /** * Identifies the best encoding for an HTTP response based on the HTTP request Accept-Encoding header. * - * @param header - the HTTP Accept-Encoding header content. - * @param preferences - an optional array of encoding strings in order of server preference. - * - * @return a string with the preferred accepted encoding. + * @param header - The HTTP Accept-Encoding header content. + * @param preferences - An optional array of encoding strings in order of server preference. + * @returns A string with the preferred accepted encoding. */ export function encoding(header?: string, preferences?: readonly string[]): string; - /** * Sorts the encodings in the HTTP request Accept-Encoding header based on client preference from most to least desired. * - * @param header - the HTTP Accept-Encoding header content. - * - * @return an array of strings of encodings sorted from the most to the least desired. + * @param header - The HTTP Accept-Encoding header content. + * @returns An array of strings of encodings sorted from the most to the least desired. */ export function encodings(header?: string): string[]; - /** * Identifies the best language for an HTTP response based on the HTTP request Accept-Language header. * - * @param header - the HTTP Accept-Language header content. - * @param preferences - an optional array of language strings in order of server preference. - * - * @return a string with the preferred accepted language. + * @param header - The HTTP Accept-Language header content. + * @param preferences - An optional array of language strings in order of server preference. + * @returns A string with the preferred accepted language. */ export function language(header?: string, preferences?: readonly string[]): string; - /** * Sorts the languages in the HTTP request Accept-Language header based on client preference from most to least desired. * - * @param header - the HTTP Accept-Language header content. - * - * @return an array of strings of languages sorted from the most to the least desired. + * @param header - The HTTP Accept-Language header content. + * @returns An array of strings of languages sorted from the most to the least desired. */ export function languages(header?: string): string[]; - /** * Identifies the best media-type for an HTTP response based on the HTTP request Accept header. * - * @param header - the HTTP Accept header content. - * @param preferences - an optional array of media-type strings in order of server preference. - * - * @return a string with the preferred accepted media-type. + * @param header - The HTTP Accept header content. + * @param preferences - An optional array of media-type strings in order of server preference. + * @returns A string with the preferred accepted media-type. */ export function mediaType(header?: string, preferences?: readonly string[]): string; - /** * Sorts the media-types in the HTTP request Accept header based on client preference from most to least desired. * - * @param header - the HTTP Accept header content. - * @param preferences - an optional array of media-type strings in order of server preference. - * - * @return an array of strings of media-types sorted from the most to the least desired. + * @param header - The HTTP Accept header content. + * @param preferences - An optional array of media-type strings in order of server preference. + * @returns An array of strings of media-types sorted from the most to the least desired. */ export function mediaTypes(header?: string, preferences?: readonly string[]): string[]; - /** * Parses the Accept-* headers of an HTTP request and returns an array of client preferences for each header. - * - * @param headers - the HTTP request headers object. - * - * @return an object with a key for each accept header result. + * + * @param headers - The HTTP request headers object. + * @returns An object with a key for each accept header result. */ export function parseAll(headers: parseAll.Headers): parseAll.Result; export namespace parseAll { - interface Headers { - readonly 'accept-charset'?: string; readonly 'accept-encoding'?: string; readonly 'accept-language'?: string; @@ -105,7 +87,6 @@ export namespace parseAll { } interface Result { - charsets: string[]; encodings: string[]; languages: string[]; diff --git a/src/index.js b/src/index.js index 5797174..9421d88 100755 --- a/src/index.js +++ b/src/index.js @@ -1,26 +1,24 @@ import * as Header from './header.js'; import * as Media from './media.js'; - const options = { charset: { - type: 'accept-charset' + type: 'accept-charset', }, encoding: { type: 'accept-encoding', default: 'identity', equivalents: new Map([ ['x-compress', 'compress'], - ['x-gzip', 'gzip'] - ]) + ['x-gzip', 'gzip'], + ]), }, language: { type: 'accept-language', - prefixMatch: true - } + prefixMatch: true, + }, }; - const charset = (header, preferences) => Header.selection(header, preferences, options.charset); const charsets = (header, preferences) => Header.selections(header, preferences, options.charset); @@ -37,16 +35,13 @@ const mediaType = (header, preferences) => Media.selection(header, preferences); const mediaTypes = (header, preferences) => Media.selections(header, preferences); - const parseAll = function (requestHeaders) { - return { charsets: charsets(requestHeaders['accept-charset']), encodings: encodings(requestHeaders['accept-encoding']), languages: languages(requestHeaders['accept-language']), - mediaTypes: mediaTypes(requestHeaders.accept) + mediaTypes: mediaTypes(requestHeaders.accept), }; }; - export { charset, charsets, encoding, encodings, language, languages, mediaType, mediaTypes, parseAll }; diff --git a/src/media.js b/src/media.js index 34d4f91..654ee09 100755 --- a/src/media.js +++ b/src/media.js @@ -1,22 +1,17 @@ -import * as Hoek from '@hapi/hoek'; import * as Boom from '@hapi/boom'; - +import * as Hoek from '@hapi/hoek'; const selection = function (header, preferences) { - const results = selections(header, preferences); return results.length ? results[0] : ''; }; - const selections = function (header, preferences) { - Hoek.assert(!preferences || Array.isArray(preferences), 'Preferences must be an array'); return parse(header, preferences); }; - // RFC 7231 Section 5.3.2 (https://tools.ietf.org/html/rfc7231#section-5.3.2) // // Accept = [ ( "," / ( media-range [ accept-params ] ) ) *( OWS "," [ OWS ( media-range [ accept-params ] ) ] ) ] @@ -43,7 +38,6 @@ const selections = function (header, preferences) { // Accept: text/*, text/plain, text/plain;format=flowed, */* // Accept: text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5 - // RFC 7231 Section 5.3.1 (https://tools.ietf.org/html/rfc7231#section-5.3.1) // // The weight is normalized to a real number in the range 0 through 1, @@ -54,13 +48,11 @@ const selections = function (header, preferences) { // weight = OWS ";" OWS "q=" qvalue // qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ) - // */* type/* type/subtype -const validMediaRx = /^(?:\*\/\*)|(?:[\w\!#\$%&'\*\+\-\.\^`\|~]+\/\*)|(?:[\w\!#\$%&'\*\+\-\.\^`\|~]+\/[\w\!#\$%&'\*\+\-\.\^`\|~]+)$/; - +const validMediaRx = + /^(?:\*\/\*)|(?:[\w\!#\$%&'\*\+\-\.\^`\|~]+\/\*)|(?:[\w\!#\$%&'\*\+\-\.\^`\|~]+\/[\w\!#\$%&'\*\+\-\.\^`\|~]+)$/; const parse = function (raw, preferences) { - // Normalize header (remove spaces and temporary remove quoted strings) const { header, quoted } = normalize(raw); @@ -73,7 +65,9 @@ const parse = function (raw, preferences) { for (let i = 0; i < parts.length; ++i) { const part = parts[i]; - if (!part) { // Ignore empty parts or leading commas + // Ignore empty parts or leading commas + + if (!part) { continue; } @@ -82,7 +76,9 @@ const parse = function (raw, preferences) { const pairs = part.split(';'); const token = pairs.shift().toLowerCase(); - if (!validMediaRx.test(token)) { // Ignore invalid types + // Ignore invalid types + + if (!validMediaRx.test(token)) { continue; } @@ -90,7 +86,7 @@ const parse = function (raw, preferences) { token, params: {}, exts: {}, - pos: i + pos: i, }; // Parse key=value @@ -98,31 +94,23 @@ const parse = function (raw, preferences) { let target = 'params'; for (const pair of pairs) { const kv = pair.split('='); - if (kv.length !== 2 || - !kv[1]) { - + if (kv.length !== 2 || !kv[1]) { throw Boom.badRequest(`Invalid accept header`); } const key = kv[0]; let value = kv[1]; - if (key === 'q' || - key === 'Q') { - + if (key === 'q' || key === 'Q') { target = 'exts'; value = parseFloat(value); - if (!Number.isFinite(value) || - value > 1 || - (value < 0.001 && value !== 0)) { - + if (!Number.isFinite(value) || value > 1 || (value < 0.001 && value !== 0)) { value = 1; } item.q = value; - } - else { + } else { if (value[0] === '"') { value = `"${quoted[value]}"`; } @@ -135,7 +123,9 @@ const parse = function (raw, preferences) { item.original = [''].concat(params.map((key) => `${key}=${item.params[key]}`)).join(';'); item.specificity = params.length; - if (item.q === undefined) { // Default no preference to q=1 (top preference) + // Default no preference to q=1 (top preference) + + if (item.q === undefined) { item.q = 1; } @@ -145,7 +135,9 @@ const parse = function (raw, preferences) { map[item.token] = item; - if (item.q) { // Skip denied selections (q=0) + // Skip denied selections (q=0) + + if (item.q) { results.push(item); } } @@ -157,20 +149,17 @@ const parse = function (raw, preferences) { return filterPreferences(map, results, preferences); }; - const normalize = function (raw) { - raw = raw || '*/*'; const normalized = { header: raw, - quoted: {} + quoted: {}, }; if (raw.includes('"')) { let i = 0; normalized.header = raw.replace(/="([^"]*)"/g, ($0, $1) => { - const key = '"' + ++i; normalized.quoted[key] = $1; return '=' + key; @@ -181,9 +170,7 @@ const normalize = function (raw) { return normalized; }; - const sort = function (a, b) { - // Sort by quality score if (b.q !== a.q) { @@ -211,9 +198,7 @@ const sort = function (a, b) { return a.pos - b.pos; }; - const innerSort = function (a, b, key) { - const aFirst = -1; const bFirst = 1; @@ -225,12 +210,10 @@ const innerSort = function (a, b, key) { return aFirst; } - return a[key] < b[key] ? aFirst : bFirst; // Group alphabetically + return a[key] < b[key] ? aFirst : bFirst; // Group alphabetically }; - const filterPreferences = function (map, results, preferences) { - // Return selections if no preferences if (!preferences?.length) { @@ -293,9 +276,7 @@ const filterPreferences = function (map, results, preferences) { if (subtype !== '*') { const pref = flat[token]; - if (pref || - (subtypes && subtypes['*'])) { - + if (pref || (subtypes && subtypes['*'])) { preferred.push((pref || token) + item.original); } @@ -316,5 +297,4 @@ const filterPreferences = function (map, results, preferences) { return preferred; }; - export { selection, selections }; From e5ce92bedf7d0699c6cc1d5d4786f891cada046c Mon Sep 17 00:00:00 2001 From: Danilo Alonso Date: Sat, 1 Aug 2026 18:15:38 -0400 Subject: [PATCH 5/7] build(deps): pin @hapi/hoek to ^12.0.0-rc.0 hoek 12 is the ESM-only release; matches the pin hapijs/topo#74 and hapijs/boom#309 set. @hapi/boom stays on 10 (still CJS), so hoek 11 remains in the tree transitively. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0b16d53..97f30d8 100755 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@hapi/boom": "^10.0.1", - "@hapi/hoek": "^11.0.2" + "@hapi/hoek": "^12.0.0-rc.0" }, "devDependencies": { "@hapi/oxc-plugin": "^1.0.2", From e7c1ecd36455e975f9274e97da1f612c557f9bf9 Mon Sep 17 00:00:00 2001 From: Danilo Alonso Date: Sat, 1 Aug 2026 19:40:11 -0400 Subject: [PATCH 6/7] build(deps): align dev toolchain with oxc-plugin 1.0.4 @hapi/oxc-plugin 1.0.4 peers on oxfmt >=0.61.0, so a fresh install hit ERESOLVE against the pinned oxfmt ^0.57.0. Versions now match the concurrent topo/boom conversions. --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 97f30d8..58ea704 100755 --- a/package.json +++ b/package.json @@ -37,13 +37,13 @@ "@hapi/hoek": "^12.0.0-rc.0" }, "devDependencies": { - "@hapi/oxc-plugin": "^1.0.2", + "@hapi/oxc-plugin": "^1.0.4", "@types/node": "^22", - "@vitest/coverage-v8": "^4.1.9", - "oxfmt": "^0.57.0", - "oxlint": "^1.72.0", + "@vitest/coverage-v8": "^4.1.10", + "oxfmt": "^0.61.0", + "oxlint": "^1.76.0", "typescript": "^6.0.3", - "vitest": "^4.1.9" + "vitest": "^4.1.10" }, "engines": { "node": ">=22" From b7b9d531600234cbbbc4c0fd27dc07e5bada892a Mon Sep 17 00:00:00 2001 From: Danilo Alonso Date: Sun, 2 Aug 2026 18:42:51 -0400 Subject: [PATCH 7/7] refactor: export at declaration site Matches the convention in the published ESM RCs (topo, boom, teamwork, file): no grouped export clause, no default export. --- src/header.js | 6 ++---- src/index.js | 20 +++++++++----------- src/media.js | 6 ++---- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/header.js b/src/header.js index 1df0e81..09b995d 100755 --- a/src/header.js +++ b/src/header.js @@ -1,12 +1,12 @@ import * as Boom from '@hapi/boom'; import * as Hoek from '@hapi/hoek'; -const selection = function (header, preferences, options) { +export const selection = function (header, preferences, options) { const results = selections(header, preferences, options); return results.length ? results[0] : ''; }; -const selections = function (header, preferences, options) { +export const selections = function (header, preferences, options) { Hoek.assert(!preferences || Array.isArray(preferences), 'Preferences must be an array'); return parse(header || '', preferences, options); @@ -204,5 +204,3 @@ const sort = function (a, b) { return a.pos - b.pos; }; - -export { selection, selections }; diff --git a/src/index.js b/src/index.js index 9421d88..29d1918 100755 --- a/src/index.js +++ b/src/index.js @@ -19,23 +19,23 @@ const options = { }, }; -const charset = (header, preferences) => Header.selection(header, preferences, options.charset); +export const charset = (header, preferences) => Header.selection(header, preferences, options.charset); -const charsets = (header, preferences) => Header.selections(header, preferences, options.charset); +export const charsets = (header, preferences) => Header.selections(header, preferences, options.charset); -const encoding = (header, preferences) => Header.selection(header, preferences, options.encoding); +export const encoding = (header, preferences) => Header.selection(header, preferences, options.encoding); -const encodings = (header, preferences) => Header.selections(header, preferences, options.encoding); +export const encodings = (header, preferences) => Header.selections(header, preferences, options.encoding); -const language = (header, preferences) => Header.selection(header, preferences, options.language); +export const language = (header, preferences) => Header.selection(header, preferences, options.language); -const languages = (header, preferences) => Header.selections(header, preferences, options.language); +export const languages = (header, preferences) => Header.selections(header, preferences, options.language); -const mediaType = (header, preferences) => Media.selection(header, preferences); +export const mediaType = (header, preferences) => Media.selection(header, preferences); -const mediaTypes = (header, preferences) => Media.selections(header, preferences); +export const mediaTypes = (header, preferences) => Media.selections(header, preferences); -const parseAll = function (requestHeaders) { +export const parseAll = function (requestHeaders) { return { charsets: charsets(requestHeaders['accept-charset']), encodings: encodings(requestHeaders['accept-encoding']), @@ -43,5 +43,3 @@ const parseAll = function (requestHeaders) { mediaTypes: mediaTypes(requestHeaders.accept), }; }; - -export { charset, charsets, encoding, encodings, language, languages, mediaType, mediaTypes, parseAll }; diff --git a/src/media.js b/src/media.js index 654ee09..bebca77 100755 --- a/src/media.js +++ b/src/media.js @@ -1,12 +1,12 @@ import * as Boom from '@hapi/boom'; import * as Hoek from '@hapi/hoek'; -const selection = function (header, preferences) { +export const selection = function (header, preferences) { const results = selections(header, preferences); return results.length ? results[0] : ''; }; -const selections = function (header, preferences) { +export const selections = function (header, preferences) { Hoek.assert(!preferences || Array.isArray(preferences), 'Preferences must be an array'); return parse(header, preferences); @@ -296,5 +296,3 @@ const filterPreferences = function (map, results, preferences) { return preferred; }; - -export { selection, selections };