Skip to content

Migrate detect os hook #5322

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions hooks/__tests__/useDetectOS.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { renderHook, waitFor } from '@testing-library/react';
import { useDetectOS } from '../useDetectOS';

const mockNavigator = {
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
userAgentData: {
getHighEntropyValues: jest.fn().mockResolvedValue({ bitness: '64' }),
},
};

const originalNavigator = global.navigator;

describe('useDetectOS', () => {
afterEach(() => {
// Reset the navigator global to the original value
Object.defineProperty(global, 'navigator', {
value: originalNavigator,
writable: true,
});
});

it('should detect the user OS and bitness', async () => {
Object.defineProperty(global, 'navigator', {
value: mockNavigator,
// Allow us to change the value of navigator for the other tests
writable: true,
});

const { result } = renderHook(() => useDetectOS());

await waitFor(() => {
expect(result.current.userOS).toBe('WIN');
});

await waitFor(() => {
expect(result.current.getDownloadLink('v18.16.0')).toBe(
'https://nodejs.org/dist/v18.16.0/node-v18.16.0-x64.msi'
);
});

expect(
mockNavigator.userAgentData.getHighEntropyValues
).toHaveBeenCalledWith(['bitness']);
});

it('should return the default url if global.navigator does not exist', async () => {
Object.defineProperty(global, 'navigator', {
value: undefined,
// Allow us to change the value of navigator for the other tests
writable: true,
});

const { result } = renderHook(() => useDetectOS());

await waitFor(() => {
expect(result.current.userOS).toBe('OTHER');
});

await waitFor(() => {
expect(result.current.getDownloadLink('v18.16.0')).toBe(
'https://nodejs.org/dist/v18.16.0/node-v18.16.0.tar.gz'
);
});
});
});
37 changes: 37 additions & 0 deletions hooks/useDetectOS.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffect, useState } from 'react';
import { detectOS } from '../util/detectOS';
import { downloadUrlByOS } from '../util/downloadUrlByOS';

import type { UserOS } from '../types/userOS';

export const useDetectOS = () => {
const [userOS, setUserOS] = useState<UserOS>('OTHER');
const [bitness, setBitness] = useState('');

useEffect(() => {
setUserOS(detectOS());

// This is necessary to detect Windows 11 on Edge.
// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/getHighEntropyValues)
// [MSFT](https://learn.microsoft.com/en-us/microsoft-edge/web-platform/how-to-detect-win11)
// @ts-expect-error no types for "userAgentData" because this API is experimental
if (typeof navigator?.userAgentData?.getHighEntropyValues === 'function') {
// @ts-expect-error no types for "userAgentData" because this API is experimental
navigator.userAgentData
.getHighEntropyValues(['bitness'])
.then((ua: { bitness: string }) => setBitness(ua.bitness))
.catch();
}
}, []);

return {
userOS,
getDownloadLink: (version: string) =>
downloadUrlByOS({
userAgent: navigator?.userAgent,
userOS,
version,
bitness,
}),
};
};
1 change: 1 addition & 0 deletions types/userOS.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type UserOS = 'MAC' | 'WIN' | 'LINUX' | 'OTHER';
31 changes: 31 additions & 0 deletions util/__tests__/detectOS.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { detectOsInUserAgent } from '../detectOS';

describe('detectOsInUserAgent', () => {
it.each([
[
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246',
'WIN',
],
[
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
'MAC',
],
[
'Mozilla/5.0 (X11; CrOS x86_64 8172.45.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.64 Safari/537.36',
'OTHER',
],
[
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1',
'LINUX',
],
[
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.3 Mobile/15E148 Safari/604.1',
'MAC',
],
['', 'OTHER'],
['OTHERAgent/1.0', 'OTHER'],
[undefined, 'OTHER'],
])('detectOsInUserAgent(%s) returns %s', (os, expected) => {
expect(detectOsInUserAgent(os)).toBe(expected);
});
});
72 changes: 72 additions & 0 deletions util/__tests__/downloadUrlByOS.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { downloadUrlByOS } from '../downloadUrlByOS';

const version = 'v18.16.0';
describe('downloadUrlByOS', () => {
it('returns the correct download URL for Mac', () => {
const userAgent =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9';
const userOS = 'MAC';
const expectedUrl = 'https://nodejs.org/dist/v18.16.0/node-v18.16.0.pkg';

expect(downloadUrlByOS({ userAgent, userOS, version })).toBe(expectedUrl);
});

it('returns the correct download URL for Windows (32-bit)', () => {
const userAgent =
'Mozilla/5.0 (Windows NT 10.0; Win32; x86) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36';
const userOS = 'WIN';
const bitness = '32';
const expectedUrl =
'https://nodejs.org/dist/v18.16.0/node-v18.16.0-x86.msi';

expect(downloadUrlByOS({ userAgent, userOS, version, bitness })).toBe(
expectedUrl
);
});

it('returns the correct download URL for Windows (64-bit) because the userAgent contains Win64', () => {
const userAgent =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246';
const userOS = 'WIN';
const bitness = '';
const expectedUrl =
'https://nodejs.org/dist/v18.16.0/node-v18.16.0-x64.msi';

expect(downloadUrlByOS({ userAgent, userOS, version, bitness })).toBe(
expectedUrl
);
});

it('returns the correct download URL for Windows (64-bit) because the userAgent contains WOW64', () => {
const userAgent =
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36';
const userOS = 'WIN';
const bitness = '';
const expectedUrl =
'https://nodejs.org/dist/v18.16.0/node-v18.16.0-x64.msi';

expect(downloadUrlByOS({ userAgent, userOS, version, bitness })).toBe(
expectedUrl
);
});

it('returns the correct download URL for Windows (64-bit) because bitness = 64', () => {
const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)';
const userOS = 'WIN';
const bitness = '64';
const expectedUrl =
'https://nodejs.org/dist/v18.16.0/node-v18.16.0-x64.msi';

expect(downloadUrlByOS({ userAgent, userOS, version, bitness })).toBe(
expectedUrl
);
});

it('returns the default download URL for other operating systems', () => {
const userAgent = 'Mozilla/5.0 (Linux; Android 11; SM-G975U1)';
const userOS = 'OTHER';
const expectedUrl = 'https://nodejs.org/dist/v18.16.0/node-v18.16.0.tar.gz';

expect(downloadUrlByOS({ userAgent, userOS, version })).toBe(expectedUrl);
});
});
19 changes: 19 additions & 0 deletions util/detectOS.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { UserOS } from '../types/userOS';

export const detectOsInUserAgent = (userAgent: string | undefined): UserOS => {
const osMatch = userAgent?.match(/(Win|Mac|Linux)/);
switch (osMatch && osMatch[1]) {
case 'Win':
return 'WIN';
case 'Mac':
return 'MAC';
case 'Linux':
return 'LINUX';
default:
return 'OTHER';
}
};

// Since `navigator.appVersion` is deprecated, we use the `userAgent``
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/appVersion
export const detectOS = (): UserOS => detectOsInUserAgent(navigator?.userAgent);
30 changes: 30 additions & 0 deletions util/downloadUrlByOS.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { UserOS } from '../types/userOS';

type DownloadUrlByOS = {
userAgent?: string | undefined;
userOS: UserOS;
version: string;
bitness?: string;
};

export const downloadUrlByOS = ({
userAgent,
userOS,
version,
bitness,
}: DownloadUrlByOS): string => {
const baseURL = `https://nodejs.org/dist/${version}`;
const is64Bit =
bitness === '64' ||
userAgent?.includes('WOW64') ||
userAgent?.includes('Win64');

switch (userOS) {
case 'MAC':
return `${baseURL}/node-${version}.pkg`;
case 'WIN':
return `${baseURL}/node-${version}-x${is64Bit ? 64 : 86}.msi`;
default:
return `${baseURL}/node-${version}.tar.gz`;
}
};