Skip to content

Commit

Permalink
Add toJSON for easy serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
yoavweiss committed Oct 9, 2023
1 parent 67e55b8 commit 2ebda54
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 14 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Change Log

## 1.1.0

- Also accept the Request object as first argument of the constructor.
- Add toJSON method to the instance for easy serialization.

## 1.0.0

Stable release: No breaking changes
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ import { ClientHints } from "client-hints";
import { userAgentParser } from "some-user-agent-parser-library";

app.get("/api-endpoint", (req, res) => {
const hints = new ClientHints(req.headers);
const hints = new ClientHints(req);

const isMobile =
hints.mobile ?? // cheap
userAgentParser(req.get("user-agent")).device?.type === "mobile"; // more expensive

res.send(isMobile ? "Mobile page" : "Desktop page");
const browserName =
hints.vendorName ?? // cheap
userAgentParser(req.get("user-agent")).browser?.name; // more expensive

console.log(JSON.stringify(hints, null, 2)); // Serialises all available hints

res.send(`${isMobile ? "Mobile" : "Desktop"} browser: ${browserName}`);
});
```
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "client-hints",
"version": "1.0.0",
"version": "1.1.0",
"description": "🕵️‍♂️ Parse client hints headers",
"keywords": [
"client-hints",
Expand Down
16 changes: 16 additions & 0 deletions src/__snapshots__/test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`client-hints general pases to JSON 1`] = `
"{
"mobile": false,
"vendorName": "Google Chrome",
"vendorVersion": "117.0.5938.132",
"platform": "macOS",
"platformVersion": "12.5.1",
"fetchSite": "none",
"fetchMode": "navigate",
"fetchDest": "document",
"fetchDestination": "document",
"fetchUser": true
}"
`;
25 changes: 15 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,15 @@ const HEADERS = {
WIDTH: "width",
};

interface Headers {
entries(): Iterable<[string, string]>;
}

export class ClientHints {
entries: [string, string][];
constructor(headers: Headers | object) {
constructor(input: Request | Headers | object) {
const headers =
input instanceof Request ? (input as Request).headers : input;
const entries =
Symbol.iterator in headers
? Array.from(
(headers as Headers).entries() as Iterable<[string, string]>,
)
headers instanceof Headers
? Array.from((headers as globalThis.Headers).entries())
: Object.entries(headers);

this.entries = entries.map(([key, value]) => [key.toLowerCase(), value]);
}

Expand Down Expand Up @@ -74,6 +69,16 @@ export class ClientHints {
return this.#store.get("uaVendorsList");
}

toJSON(): Record<string, string | number | boolean> {
return Object.getOwnPropertyNames(Object.getPrototypeOf(this))
.filter((name) => name !== "constructor")
.reduce(
(acc, name) =>
this[name] !== "" ? Object.assign(acc, { [name]: this[name] }) : acc,
{},
);
}

get mobile(): boolean {
return this.#get(HEADERS.MOBILE)?.includes("?1");
}
Expand Down
29 changes: 29 additions & 0 deletions src/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ describe("client-hints", () => {
["downlink", "Downlink", "1.7", 1.7],
["platform", "Sec-CH-UA-Platform", '"macOS"', "macOS"],
["platformVersion", "Sec-CH-UA-Platform-Version", '"12.1.0"', "12.1.0"],
["purpose", "Sec-Purpose", "prefetch", "prefetch"],
].forEach(
([feature, header, value, expected]: [string, string, string, any]) => {
it([feature, header].join(": "), () => {
Expand All @@ -94,5 +95,33 @@ describe("client-hints", () => {
});
},
);
it("accepts request object", () => {
const request = new Request("https://example.com", {
headers: {
"Sec-CH-UA-Platform": "macOS",
},
});
const hints = new ClientHints(request);
assert.equal(hints.platform, "macOS");
});
it("pases to JSON", () => {
const headers = new Headers({
"sec-ch-ua":
'"Google Chrome";v="117", "Not;A=Brand";v="8", "Chromium";v="117"',
"sec-ch-ua-full-version": '"117.0.5938.132"',
"sec-ch-ua-full-version-list":
'"Google Chrome";v="117.0.5938.132", "Not;A=Brand";v="8.0.0.0", "Chromium";v="117.0.5938.132"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-model": '""',
"sec-ch-ua-platform": '"macOS"',
"sec-ch-ua-platform-version": '"12.5.1"',
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
});
const hints = new ClientHints(headers);
expect(JSON.stringify(hints, null, 2)).toMatchSnapshot();
});
});
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["esnext", "dom"],
"lib": ["esnext", "dom", "dom.iterable"],
"module": "esnext",
"types": ["@types/node", "@types/jest"],
"esModuleInterop": true,
Expand Down

0 comments on commit 2ebda54

Please sign in to comment.